jazzy 0.13.5 → 0.13.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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