Skip to content

Commit

Permalink
Swift concurrency support (#1258)
Browse files Browse the repository at this point in the history
* Identify actors
* Identify async methods and properties
* Syntax highlighter workaround for 5.5
  • Loading branch information
johnfairh committed Sep 28, 2021
1 parent 9badbe3 commit 4e1cd27
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -17,6 +17,10 @@
[Brian Osborn](https://github.com/bosborn)
[John Fairhurst](https://github.com/johnfairh)

* Support Swift concurrency features: identify actors and asynchronous
methods.
[John Fairhurst](https://github.com/johnfairh)

##### Bug Fixes

* None.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Expand Up @@ -49,7 +49,7 @@ git push
You'll need push access to the integration specs repo to do this. You can
request access from one of the maintainers when filing your PR.

You must have Xcode 12.5 installed to build the integration specs.
You must have Xcode 13 installed to build the integration specs.

## Making changes to SourceKitten

Expand Down
2 changes: 2 additions & 0 deletions lib/jazzy/doc_builder.rb
Expand Up @@ -370,6 +370,8 @@ def self.render_item(item, source_module)
deprecation_message: item.deprecation_message,
unavailable_message: item.unavailable_message,
usage_discouraged: item.usage_discouraged?,
async: item.async,
declaration_note: item.declaration_note,
}
end
# rubocop:enable Metrics/MethodLength
Expand Down
12 changes: 12 additions & 0 deletions lib/jazzy/highlighter.rb
Expand Up @@ -2,6 +2,18 @@

require 'rouge'

# While Rouge is downlevel (Rouge PR#1715 unreleased)
module Rouge
module Lexers
class Swift
prepend :root do
rule(/\b(?:async|await|isolated)\b/, Keyword)
rule(/\b(?:actor|nonisolated)\b/, Keyword::Declaration)
end
end
end
end

module Jazzy
# This module helps highlight code
module Highlighter
Expand Down
9 changes: 9 additions & 0 deletions lib/jazzy/source_declaration.rb
Expand Up @@ -138,6 +138,7 @@ def display_other_language_declaration
attr_accessor :unavailable_message
attr_accessor :generic_requirements
attr_accessor :inherited_types
attr_accessor :async

def usage_discouraged?
unavailable || deprecated
Expand Down Expand Up @@ -188,6 +189,14 @@ def type_from_doc_module?
(swift? && usr && modulename.nil?)
end

# Info text for contents page by collapsed item name
def declaration_note
notes = [default_impl_abstract ? 'default implementation' : nil,
from_protocol_extension ? 'extension method' : nil,
async ? 'asynchronous' : nil].compact
notes.join(', ').humanize unless notes.empty?
end

def alternative_abstract
if file = alternative_abstract_file
Pathname(file).read
Expand Down
23 changes: 21 additions & 2 deletions lib/jazzy/source_declaration/type.rb
Expand Up @@ -12,11 +12,24 @@ def self.all

attr_reader :kind

def initialize(kind)
def initialize(kind, declaration = nil)
kind = fixup_kind(kind, declaration) if declaration
@kind = kind
@type = TYPES[kind]
end

# Improve kind from full declaration
def fixup_kind(kind, declaration)
if kind == 'source.lang.swift.decl.class' &&
declaration.include?(
'<syntaxtype.keyword>actor</syntaxtype.keyword>',
)
'source.lang.swift.decl.actor'
else
kind
end
end

def dash_type
@type && @type[:dash]
end
Expand Down Expand Up @@ -115,7 +128,8 @@ def swift_extension?
end

def swift_extensible?
kind =~ /^source\.lang\.swift\.decl\.(class|struct|protocol|enum)$/
kind =~
/^source\.lang\.swift\.decl\.(class|struct|protocol|enum|actor)$/
end

def swift_protocol?
Expand Down Expand Up @@ -274,6 +288,11 @@ def ==(other)
}.freeze,

# Swift
'source.lang.swift.decl.actor' => {
jazzy: 'Actor',
dash: 'Actor',
global: true,
}.freeze,
'source.lang.swift.decl.function.accessor.address' => {
jazzy: 'Addressor',
dash: 'Function',
Expand Down
21 changes: 19 additions & 2 deletions lib/jazzy/sourcekitten.rb
Expand Up @@ -524,6 +524,14 @@ def self.make_swift_declaration(doc, declaration)
.join("\n")
end

# Exclude non-async routines that accept async closures
def self.swift_async?(fully_annotated_decl)
document = REXML::Document.new(fully_annotated_decl)
!document.elements['/*/syntaxtype.keyword[text()="async"]'].nil?
rescue StandardError
nil
end

# Strip default property attributes because libclang
# adds them all, even if absent in the original source code.
DEFAULT_ATTRIBUTES = %w[atomic readwrite assign unsafe_unretained].freeze
Expand Down Expand Up @@ -565,7 +573,9 @@ def self.make_source_declarations(docs, parent = nil, mark = SourceMark.new)
end
declaration = SourceDeclaration.new
declaration.parent_in_code = parent
declaration.type = SourceDeclaration::Type.new(doc['key.kind'])
declaration.type =
SourceDeclaration::Type.new(doc['key.kind'],
doc['key.fully_annotated_decl'])
declaration.typename = doc['key.typename']
declaration.objc_name = doc['key.name']
documented_name = if Config.instance.hide_objc? && doc['key.swift_name']
Expand Down Expand Up @@ -610,6 +620,11 @@ def self.make_source_declarations(docs, parent = nil, mark = SourceMark.new)
inherited_types = doc['key.inheritedtypes'] || []
declaration.inherited_types =
inherited_types.map { |type| type['key.name'] }.compact
declaration.async =
doc['key.symgraph_async'] ||
if xml_declaration = doc['key.fully_annotated_decl']
swift_async?(xml_declaration)
end

next unless make_doc_info(doc, declaration)

Expand Down Expand Up @@ -819,7 +834,9 @@ def self.mark_and_merge_protocol_extensions(protocol, extensions)
extensions.each do |ext|
ext.children = ext.children.select do |ext_member|
proto_member = protocol.children.find do |p|
p.name == ext_member.name && p.type == ext_member.type
p.name == ext_member.name &&
p.type == ext_member.type &&
p.async == ext_member.async
end

# Extension-only method, keep.
Expand Down
7 changes: 6 additions & 1 deletion lib/jazzy/symbol_graph/graph.rb
Expand Up @@ -78,8 +78,13 @@ def rel_source_name(rel, source_node)

# Protocol conformance is redundant if it's unconditional
# and already expressed in the type's declaration.
#
# Skip implementation-detail conformances.
def redundant_conformance?(rel, type, protocol)
type && rel.constraints.empty? && type.conformance?(protocol)
return false unless type

(rel.constraints.empty? && type.conformance?(protocol)) ||
(type.actor? && rel.actor_protocol?)
end

# source is a member/protocol requirement of target
Expand Down
6 changes: 6 additions & 0 deletions lib/jazzy/symbol_graph/relationship.rb
Expand Up @@ -22,6 +22,12 @@ def default_implementation?
kind == :defaultImplementationOf
end

# Protocol conformances added by compiler to actor decls that
# users aren't interested in.
def actor_protocol?
%w[Actor Sendable].include?(target_fallback)
end

def initialize(hash)
kind = hash[:kind]
unless KINDS.include?(kind)
Expand Down
12 changes: 12 additions & 0 deletions lib/jazzy/symbol_graph/sym_node.rb
Expand Up @@ -25,6 +25,7 @@ def children_to_sourcekit

# 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
Expand Down Expand Up @@ -61,6 +62,10 @@ def protocol?
symbol.kind.end_with?('protocol')
end

def actor?
symbol.kind.end_with?('actor')
end

def constraints
symbol.constraints
end
Expand Down Expand Up @@ -112,6 +117,11 @@ def inherits_clause
" : #{superclass_name}"
end

# approximately...
def async?
symbol.declaration =~ /\basync\b[^)]*$/
end

def full_declaration
symbol.attributes
.append(symbol.declaration + inherits_clause + where_clause)
Expand All @@ -130,6 +140,7 @@ def to_sourcekit
'key.accessibility' => symbol.acl,
'key.parsed_decl' => declaration,
'key.annotated_decl' => xml_declaration,
'key.symgraph_async' => async?,
}
if docs = symbol.doc_comments
hash['key.doc.comment'] = docs
Expand Down Expand Up @@ -160,5 +171,6 @@ def <=>(other)
symbol <=> other.symbol
end
end
# rubocop:enable Metrics/ClassLength
end
end
27 changes: 21 additions & 6 deletions lib/jazzy/symbol_graph/symbol.rb
Expand Up @@ -25,8 +25,8 @@ def name
def initialize(hash)
self.usr = hash[:identifier][:precise]
self.path_components = hash[:pathComponents]
raw_decl = hash[:declarationFragments].map { |f| f[:spelling] }.join
init_kind(hash[:kind][:identifier])
raw_decl, keywords = parse_decl_fragments(hash[:declarationFragments])
init_kind(hash[:kind][:identifier], keywords)
init_declaration(raw_decl)
if func_signature = hash[:functionSignature]
init_func_signature(func_signature)
Expand All @@ -44,6 +44,16 @@ def initialize(hash)
init_generic_type_params(hash)
end

def parse_decl_fragments(fragments)
decl = ''
keywords = Set.new
fragments.each do |frag|
decl += frag[:spelling]
keywords.add(frag[:spelling]) if frag[:kind] == 'keyword'
end
[decl, keywords]
end

# Repair problems with SymbolGraph's declprinter

def init_declaration(raw_decl)
Expand Down Expand Up @@ -89,17 +99,22 @@ def init_func_signature(func_signature)
'static.subscript' => 'function.subscript',
'typealias' => 'typealias',
'associatedtype' => 'associatedtype',
'actor' => 'actor',
}.freeze

# We treat 'static var' differently to 'class var'
def adjust_kind_for_declaration(kind)
return kind unless declaration =~ /\bstatic\b/
# We treat actors as first-class entities
def adjust_kind_for_declaration(kind, keywords)
if kind == 'swift.class' && keywords.member?('actor')
return 'swift.actor'
end
return kind unless keywords.member?('static')

kind.gsub(/type/, 'static')
end

def init_kind(kind)
adjusted = adjust_kind_for_declaration(kind)
def init_kind(kind, keywords)
adjusted = adjust_kind_for_declaration(kind, keywords)
sourcekit_kind = KIND_MAP[adjusted.sub('swift.', '')]
raise "Unknown symbol kind '#{kind}'" unless sourcekit_kind

Expand Down
11 changes: 3 additions & 8 deletions lib/jazzy/themes/apple/templates/task.mustache
Expand Up @@ -28,16 +28,11 @@
<a class="token discouraged" href="#/{{usr}}">{{{name_html}}}</a>
{{/usage_discouraged}}
</code>
{{#default_impl_abstract}}
{{#declaration_note}}
<span class="declaration-note">
Default implementation
{{.}}
</span>
{{/default_impl_abstract}}
{{#from_protocol_extension}}
<span class="declaration-note">
Extension method
</span>
{{/from_protocol_extension}}
{{/declaration_note}}
</div>
<div class="height-container">
<div class="pointer-container"></div>
Expand Down
11 changes: 3 additions & 8 deletions lib/jazzy/themes/fullwidth/templates/task.mustache
Expand Up @@ -28,16 +28,11 @@
<a class="token discouraged" href="#/{{usr}}">{{{name_html}}}</a>
{{/usage_discouraged}}
</code>
{{#default_impl_abstract}}
{{#declaration_note}}
<span class="declaration-note">
Default implementation
{{.}}
</span>
{{/default_impl_abstract}}
{{#from_protocol_extension}}
<span class="declaration-note">
Extension method
</span>
{{/from_protocol_extension}}
{{/declaration_note}}
</div>
<div class="height-container">
<div class="pointer-container"></div>
Expand Down
11 changes: 3 additions & 8 deletions lib/jazzy/themes/jony/templates/task.mustache
Expand Up @@ -28,16 +28,11 @@
<a class="token discouraged" href="#/{{usr}}">{{{name_html}}}</a>
{{/usage_discouraged}}
</code>
{{#default_impl_abstract}}
{{#declaration_note}}
<span class="declaration-note">
Default implementation
{{.}}
</span>
{{/default_impl_abstract}}
{{#from_protocol_extension}}
<span class="declaration-note">
Extension method
</span>
{{/from_protocol_extension}}
{{/declaration_note}}
</div>
<div class="height-container">
<div class="pointer-container"></div>
Expand Down
2 changes: 1 addition & 1 deletion spec/integration_specs
Submodule integration_specs updated 261 files

0 comments on commit 4e1cd27

Please sign in to comment.