jazzy 0.13.5 → 0.13.6

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.
@@ -0,0 +1,94 @@
1
+ module Jazzy
2
+ module SymbolGraph
3
+ # Constraint is a tidied-up JSON object, used by both Symbol and
4
+ # Relationship, and key to reconstructing extensions.
5
+ class Constraint
6
+ attr_accessor :kind
7
+ attr_accessor :lhs
8
+ attr_accessor :rhs
9
+
10
+ private
11
+
12
+ def initialize(kind, lhs, rhs)
13
+ self.kind = kind # "==" or ":"
14
+ self.lhs = lhs
15
+ self.rhs = rhs
16
+ end
17
+
18
+ public
19
+
20
+ KIND_MAP = {
21
+ 'conformance' => ':',
22
+ 'superclass' => ':',
23
+ 'sameType' => '==',
24
+ }.freeze
25
+
26
+ # Init from a JSON hash
27
+ def self.new_hash(hash)
28
+ kind = KIND_MAP[hash[:kind]]
29
+ raise "Unknown constraint kind '#{kind}'" unless kind
30
+ lhs = hash[:lhs].sub(/^Self\./, '')
31
+ rhs = hash[:rhs].sub(/^Self\./, '')
32
+ new(kind, lhs, rhs)
33
+ end
34
+
35
+ # Init from a Swift declaration fragment eg. 'A : B'
36
+ def self.new_declaration(decl)
37
+ decl =~ /^(.*?)\s*([:<=]+)\s*(.*)$/
38
+ new(Regexp.last_match[2],
39
+ Regexp.last_match[1],
40
+ Regexp.last_match[3])
41
+ end
42
+
43
+ def to_swift
44
+ "#{lhs} #{kind} #{rhs}"
45
+ end
46
+
47
+ # The first component of types in the constraint
48
+ def type_names
49
+ Set.new([lhs, rhs].map { |n| n.sub(/\..*$/, '') })
50
+ end
51
+
52
+ def self.new_list(hash_list)
53
+ hash_list.map { |h| Constraint.new_hash(h) }.sort.uniq
54
+ end
55
+
56
+ # Swift protocols and reqs have an implementation/hidden conformance
57
+ # to their own protocol: we don't want to think about this in docs.
58
+ def self.new_list_for_symbol(hash_list, path_components)
59
+ hash_list.map do |hash|
60
+ if hash[:lhs] == 'Self' &&
61
+ hash[:kind] == 'conformance' &&
62
+ path_components.include?(hash[:rhs])
63
+ next nil
64
+ end
65
+ Constraint.new_hash(hash)
66
+ end.compact
67
+ end
68
+
69
+ # Workaround Swift 5.3 bug with missing constraint rels
70
+ def self.new_list_from_declaration(decl)
71
+ decl.split(/\s*,\s*/).map { |cons| Constraint.new_declaration(cons) }
72
+ end
73
+
74
+ # Sort order - by Swift text
75
+ include Comparable
76
+
77
+ def <=>(other)
78
+ to_swift <=> other.to_swift
79
+ end
80
+
81
+ alias eql? ==
82
+
83
+ def hash
84
+ to_swift.hash
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ class Array
91
+ def to_where_clause
92
+ empty? ? '' : ' where ' + map(&:to_swift).join(', ')
93
+ end
94
+ end
@@ -0,0 +1,114 @@
1
+ module Jazzy
2
+ module SymbolGraph
3
+ # For extensions we need to track constraints of the extended type
4
+ # and the constraints introduced by the extension.
5
+ class ExtConstraints
6
+ attr_accessor :type # array
7
+ attr_accessor :ext # array
8
+
9
+ # all constraints inherited by members of the extension
10
+ def merged
11
+ (type + ext).sort
12
+ end
13
+
14
+ def initialize(type_constraints, ext_constraints)
15
+ self.type = type_constraints || []
16
+ self.ext = ext_constraints || []
17
+ end
18
+ end
19
+
20
+ # An ExtNode is a node of the reconstructed syntax tree representing
21
+ # an extension that we fabricate to resolve certain relationships.
22
+ class ExtNode < BaseNode
23
+ attr_accessor :usr
24
+ attr_accessor :name
25
+ attr_accessor :all_constraints # ExtConstraints
26
+ attr_accessor :conformances # array, can be empty
27
+
28
+ # Deduce an extension from a member of an unknown type or
29
+ # of known type with additional constraints
30
+ def self.new_for_member(type_usr,
31
+ member,
32
+ constraints)
33
+ new(type_usr,
34
+ member.parent_qualified_name,
35
+ constraints).tap { |o| o.add_child(member) }
36
+ end
37
+
38
+ # Deduce an extension from a protocol conformance for some type
39
+ def self.new_for_conformance(type_usr,
40
+ type_name,
41
+ protocol,
42
+ constraints)
43
+ new(type_usr, type_name, constraints).tap do |o|
44
+ o.add_conformance(protocol)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def initialize(usr, name, constraints)
51
+ self.usr = usr
52
+ self.name = name
53
+ self.all_constraints = constraints
54
+ self.conformances = []
55
+ super()
56
+ end
57
+
58
+ public
59
+
60
+ def constraints
61
+ all_constraints.merged
62
+ end
63
+
64
+ def add_conformance(protocol)
65
+ conformances.append(protocol).sort!
66
+ end
67
+
68
+ def full_declaration
69
+ decl = "extension #{name}"
70
+ unless conformances.empty?
71
+ decl += ' : ' + conformances.join(', ')
72
+ end
73
+ decl + all_constraints.ext.to_where_clause
74
+ end
75
+
76
+ def to_sourcekit(module_name)
77
+ declaration = full_declaration
78
+ xml_declaration = "<swift>#{CGI.escapeHTML(declaration)}</swift>"
79
+
80
+ hash = {
81
+ 'key.kind' => 'source.lang.swift.decl.extension',
82
+ 'key.usr' => usr,
83
+ 'key.name' => name,
84
+ 'key.modulename' => module_name,
85
+ 'key.parsed_declaration' => declaration,
86
+ 'key.annotated_decl' => xml_declaration,
87
+ }
88
+
89
+ unless conformances.empty?
90
+ hash['key.inheritedtypes'] = conformances.map do |conformance|
91
+ { 'key.name' => conformance }
92
+ end
93
+ end
94
+
95
+ unless children.empty?
96
+ hash['key.substructure'] = children_to_sourcekit
97
+ end
98
+
99
+ hash
100
+ end
101
+
102
+ # Sort order - by type name then constraint
103
+ include Comparable
104
+
105
+ def sort_key
106
+ name + constraints.map(&:to_swift).join
107
+ end
108
+
109
+ def <=>(other)
110
+ sort_key <=> other.sort_key
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,193 @@
1
+ # rubocop:disable Metrics/ClassLength
2
+ module Jazzy
3
+ module SymbolGraph
4
+ # A Graph is the coordinator to import a symbolgraph json file.
5
+ # Deserialize it to Symbols and Relationships, then rebuild
6
+ # the AST shape using SymNodes and ExtNodes and extract SourceKit json.
7
+ class Graph
8
+ attr_accessor :module_name
9
+ attr_accessor :symbol_nodes # usr -> SymNode
10
+ attr_accessor :relationships # [Relationship]
11
+ attr_accessor :ext_nodes # (usr, constraints) -> ExtNode
12
+
13
+ # Parse the JSON into flat tables of data
14
+ def initialize(json, module_name)
15
+ self.module_name = module_name
16
+ graph = JSON.parse(json, symbolize_names: true)
17
+
18
+ self.symbol_nodes = {}
19
+ graph[:symbols].each do |hash|
20
+ symbol = Symbol.new(hash)
21
+ symbol_nodes[symbol.usr] = SymNode.new(symbol)
22
+ end
23
+
24
+ self.relationships =
25
+ graph[:relationships].map { |hash| Relationship.new(hash) }
26
+
27
+ self.ext_nodes = {}
28
+ end
29
+
30
+ # ExtNode index. (type USR, extension constraints) -> ExtNode.
31
+ # This minimizes the number of extensions
32
+
33
+ def ext_key(usr, constraints)
34
+ usr + constraints.map(&:to_swift).join
35
+ end
36
+
37
+ def add_ext_member(type_usr, member_node, constraints)
38
+ key = ext_key(type_usr, constraints.ext)
39
+ if ext_node = ext_nodes[key]
40
+ ext_node.add_child(member_node)
41
+ else
42
+ ext_nodes[key] =
43
+ ExtNode.new_for_member(type_usr, member_node, constraints)
44
+ end
45
+ end
46
+
47
+ def add_ext_conformance(type_usr,
48
+ type_name,
49
+ protocol,
50
+ constraints)
51
+ key = ext_key(type_usr, constraints.ext)
52
+ if ext_node = ext_nodes[key]
53
+ ext_node.add_conformance(protocol)
54
+ else
55
+ ext_nodes[key] =
56
+ ExtNode.new_for_conformance(type_usr,
57
+ type_name,
58
+ protocol,
59
+ constraints)
60
+ end
61
+ end
62
+
63
+ # Increasingly desparate ways to find the name of the symbol
64
+ # at the target end of a relationship
65
+ def rel_target_name(rel, target_node)
66
+ (target_node && target_node.symbol.name) ||
67
+ rel.target_fallback ||
68
+ Jazzy::SymbolGraph.demangle(rel.target_usr)
69
+ end
70
+
71
+ # Same for the source end. Less help from the tool here
72
+ def rel_source_name(rel, source_node)
73
+ (source_node && source_node.qualified_name) ||
74
+ Jazzy::SymbolGraph.demangle(rel.source_usr)
75
+ end
76
+
77
+ # Protocol conformance is redundant if it's unconditional
78
+ # and already expressed in the type's declaration.
79
+ def redundant_conformance?(rel, type, protocol)
80
+ type && rel.constraints.empty? && type.conformance?(protocol)
81
+ end
82
+
83
+ # source is a member/protocol requirement of target
84
+ def rebuild_member(rel, source, target)
85
+ return unless source
86
+
87
+ source.protocol_requirement = rel.protocol_requirement?
88
+ constraints =
89
+ ExtConstraints.new(target && target.constraints,
90
+ source.unique_context_constraints(target))
91
+
92
+ # Add to its parent or invent an extension
93
+ unless target && target.try_add_child(source, constraints.ext)
94
+ add_ext_member(rel.target_usr, source, constraints)
95
+ end
96
+ end
97
+
98
+ # "source : target" either from type decl or ext decl
99
+ def rebuild_conformance(rel, source, target)
100
+ protocol_name = rel_target_name(rel, target)
101
+
102
+ return if redundant_conformance?(rel, source, protocol_name)
103
+
104
+ type_constraints = (source && source.constraints) || []
105
+ constraints =
106
+ ExtConstraints.new(type_constraints,
107
+ rel.constraints - type_constraints)
108
+
109
+ # Create an extension or enhance an existing one
110
+ add_ext_conformance(rel.source_usr,
111
+ rel_source_name(rel, source),
112
+ protocol_name,
113
+ constraints)
114
+ end
115
+
116
+ # "source is a default implementation of protocol requirement target"
117
+ def rebuild_default_implementation(_rel, source, target)
118
+ return unless source
119
+
120
+ unless target &&
121
+ (target_parent = target.parent) &&
122
+ target_parent.is_a?(SymNode)
123
+ # Could probably figure this out with demangle, but...
124
+ warn "Can't resolve membership of default implementation "\
125
+ "#{source.symbol.usr}."
126
+ source.unlisted = true
127
+ return
128
+ end
129
+ constraints =
130
+ ExtConstraints.new(target_parent.constraints,
131
+ source.unique_context_constraints(target_parent))
132
+
133
+ add_ext_member(target_parent.symbol.usr,
134
+ source,
135
+ constraints)
136
+ end
137
+
138
+ # "source is a class that inherits from target"
139
+ def rebuild_inherits(_rel, source, target)
140
+ if source && target
141
+ source.superclass_name = target.symbol.name
142
+ end
143
+ end
144
+
145
+ # Process a structural relationship to link nodes
146
+ def rebuild_rel(rel)
147
+ source = symbol_nodes[rel.source_usr]
148
+ target = symbol_nodes[rel.target_usr]
149
+
150
+ case rel.kind
151
+ when :memberOf, :optionalRequirementOf, :requirementOf
152
+ rebuild_member(rel, source, target)
153
+
154
+ when :conformsTo
155
+ rebuild_conformance(rel, source, target)
156
+
157
+ when :defaultImplementationOf
158
+ rebuild_default_implementation(rel, source, target)
159
+
160
+ when :inheritsFrom
161
+ rebuild_inherits(rel, source, target)
162
+ end
163
+ # don't seem to care about:
164
+ # - overrides: not bothered, also unimplemented for protocols
165
+ end
166
+
167
+ # Rebuild the AST structure and convert to SourceKit
168
+ def to_sourcekit
169
+ # Do default impls after the others so we can find protocol
170
+ # type nodes from protocol requirements.
171
+ default_impls, other_rels =
172
+ relationships.partition(&:default_implementation?)
173
+ (other_rels + default_impls).each { |r| rebuild_rel(r) }
174
+
175
+ root_symbol_nodes =
176
+ symbol_nodes.values
177
+ .select(&:top_level_decl?)
178
+ .sort
179
+ .map(&:to_sourcekit)
180
+
181
+ root_ext_nodes =
182
+ ext_nodes.values
183
+ .sort
184
+ .map { |n| n.to_sourcekit(module_name) }
185
+ {
186
+ 'key.diagnostic_stage' => 'parse',
187
+ 'key.substructure' => root_symbol_nodes + root_ext_nodes,
188
+ }
189
+ end
190
+ end
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,39 @@
1
+ module Jazzy
2
+ module SymbolGraph
3
+ # A Relationship is a tidied-up SymbolGraph JSON object
4
+ class Relationship
5
+ attr_accessor :kind
6
+ attr_accessor :source_usr
7
+ attr_accessor :target_usr
8
+ attr_accessor :target_fallback # can be nil
9
+ attr_accessor :constraints # array, can be empty
10
+
11
+ KINDS = %w[memberOf conformsTo defaultImplementationOf
12
+ overrides inheritsFrom requirementOf
13
+ optionalRequirementOf].freeze
14
+
15
+ def protocol_requirement?
16
+ %i[requirementOf optionalRequirementOf].include? kind
17
+ end
18
+
19
+ def default_implementation?
20
+ kind == :defaultImplementationOf
21
+ end
22
+
23
+ def initialize(hash)
24
+ kind = hash[:kind]
25
+ unless KINDS.include?(kind)
26
+ raise "Unknown relationship kind '#{kind}'"
27
+ end
28
+ self.kind = kind.to_sym
29
+ self.source_usr = hash[:source]
30
+ self.target_usr = hash[:target]
31
+ if fallback = hash[:targetFallback]
32
+ # Strip the leading module name
33
+ self.target_fallback = fallback.sub(/^.*?\./, '')
34
+ end
35
+ self.constraints = Constraint.new_list(hash[:swiftConstraints] || [])
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,154 @@
1
+ module Jazzy
2
+ module SymbolGraph
3
+ # The rebuilt syntax tree is made of nodes that either match
4
+ # symbols or that we fabricate for extensions. This is the common
5
+ # treeishness.
6
+ class BaseNode
7
+ attr_accessor :children # array, can be empty
8
+ attr_accessor :parent # can be nil
9
+
10
+ def initialize
11
+ self.children = []
12
+ end
13
+
14
+ def add_child(child)
15
+ child.parent = self
16
+ children.append(child)
17
+ end
18
+
19
+ def children_to_sourcekit
20
+ children.sort.map(&:to_sourcekit)
21
+ end
22
+ end
23
+
24
+ # A SymNode is a node of the reconstructed syntax tree holding a symbol.
25
+ # It can turn itself into SourceKit and helps decode extensions.
26
+ class SymNode < BaseNode
27
+ attr_accessor :symbol
28
+ attr_writer :override
29
+ attr_writer :protocol_requirement
30
+ attr_writer :unlisted
31
+ attr_accessor :superclass_name
32
+
33
+ def override?
34
+ @override
35
+ end
36
+
37
+ def protocol_requirement?
38
+ @protocol_requirement
39
+ end
40
+
41
+ def top_level_decl?
42
+ !@unlisted && parent.nil?
43
+ end
44
+
45
+ def initialize(symbol)
46
+ self.symbol = symbol
47
+ super()
48
+ end
49
+
50
+ def qualified_name
51
+ symbol.path_components.join('.')
52
+ end
53
+
54
+ def parent_qualified_name
55
+ symbol.path_components[0...-1].join('.')
56
+ end
57
+
58
+ def protocol?
59
+ symbol.kind.end_with?('protocol')
60
+ end
61
+
62
+ def constraints
63
+ symbol.constraints
64
+ end
65
+
66
+ # Add another SymNode as a member if possible.
67
+ # It must go in an extension if either:
68
+ # - it has different generic constraints to us; or
69
+ # - we're a protocol and it's a default impl / ext method
70
+ def try_add_child(node, unique_context_constraints)
71
+ unless unique_context_constraints.empty? &&
72
+ (!protocol? || node.protocol_requirement?)
73
+ return false
74
+ end
75
+ add_child(node)
76
+ true
77
+ end
78
+
79
+ # The `Constraint`s on this decl that are both:
80
+ # 1. Unique, ie. not just inherited from its context; and
81
+ # 2. Constraining the *context's* gen params rather than our own.
82
+ def unique_context_constraints(context)
83
+ return symbol.constraints unless context
84
+
85
+ new_generic_type_params =
86
+ symbol.generic_type_params - context.symbol.generic_type_params
87
+
88
+ (symbol.constraints - context.symbol.constraints)
89
+ .select { |con| con.type_names.disjoint?(new_generic_type_params) }
90
+ end
91
+
92
+ # Messy check whether we need to fabricate an extension for a protocol
93
+ # conformance: don't bother if it's already in the type declaration.
94
+ def conformance?(protocol)
95
+ return false unless symbol.declaration =~ /(?<=:).*?(?=(where|$))/
96
+ Regexp.last_match[0] =~ /\b#{protocol}\b/
97
+ end
98
+
99
+ # Generate the 'where' clause for the declaration
100
+ def where_clause
101
+ parent_constraints = (parent && parent.constraints) || []
102
+ (constraints - parent_constraints).to_where_clause
103
+ end
104
+
105
+ def inherits_clause
106
+ return '' unless superclass_name
107
+ " : #{superclass_name}"
108
+ end
109
+
110
+ def full_declaration
111
+ symbol.availability
112
+ .append(symbol.declaration + inherits_clause + where_clause)
113
+ .join("\n")
114
+ end
115
+
116
+ # rubocop:disable Metrics/MethodLength
117
+ def to_sourcekit
118
+ declaration = full_declaration
119
+ xml_declaration = "<swift>#{CGI.escapeHTML(declaration)}</swift>"
120
+
121
+ hash = {
122
+ 'key.kind' => symbol.kind,
123
+ 'key.usr' => symbol.usr,
124
+ 'key.name' => symbol.name,
125
+ 'key.accessibility' => symbol.acl,
126
+ 'key.parsed_decl' => declaration,
127
+ 'key.annotated_decl' => xml_declaration,
128
+ }
129
+ if docs = symbol.doc_comments
130
+ hash['key.doc.comment'] = docs
131
+ hash['key.doc.full_as_xml'] = ''
132
+ end
133
+ if location = symbol.location
134
+ hash['key.filepath'] = location[:filename]
135
+ hash['key.doc.line'] = location[:line]
136
+ hash['key.doc.column'] = location[:character]
137
+ end
138
+ unless children.empty?
139
+ hash['key.substructure'] = children_to_sourcekit
140
+ end
141
+
142
+ hash
143
+ end
144
+ # rubocop:enable Metrics/MethodLength
145
+
146
+ # Sort order - by symbol
147
+ include Comparable
148
+
149
+ def <=>(other)
150
+ symbol <=> other.symbol
151
+ end
152
+ end
153
+ end
154
+ end