api_diff 0.2.0 → 0.3.0
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.
- checksums.yaml +4 -4
- data/README.md +6 -3
- data/lib/api_diff/api.rb +16 -8
- data/lib/api_diff/cli.rb +8 -3
- data/lib/api_diff/interface.rb +7 -0
- data/lib/api_diff/kotlin_bcv_parser.rb +33 -26
- data/lib/api_diff/parser.rb +5 -2
- data/lib/api_diff/swift_interface_parser.rb +76 -41
- data/lib/api_diff/type.rb +16 -7
- data/lib/api_diff/version.rb +1 -1
- data/test/kotlin_bcv_parser_test.rb +34 -9
- data/test/swift_interface_parser_test.rb +142 -13
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29d7124fb871227197ac950b3b042ae3a6a3c99eafaf0bc0762e8dceefed24fc
|
4
|
+
data.tar.gz: f10484ee00caad08e50bbb217bc5fd3308f4414e780cb93a9a3f4177ea69cd29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3966edcf349cc3e43deead3ee42e15449b4278d7eb2f545d00f0a7607cde01b38abb629790dd01252bec9b56d1327a3dd127f5c274765fa1d9882767a9c8f133
|
7
|
+
data.tar.gz: 980f1818d60f005d69c626e1749c84254b41bf184ff37dd9bc1872c3ba80c8dc96d9922a3be3a0f3be27dc27553de4541fe2a5c9a5529a2cc16bac96f0d74262
|
data/README.md
CHANGED
@@ -31,15 +31,18 @@ gem install api_diff
|
|
31
31
|
All output is printed to `STDOUT`.
|
32
32
|
|
33
33
|
- `--format`: required - specifies the format of the input file (see below).
|
34
|
-
- `--
|
34
|
+
- `--short-names`: optional - shorten type references by removing package qualifiers.
|
35
35
|
- `--normalize`: optional - transform API to a common, cross-language baseline. Less accurate when comparing APIs of the same language but helpful when comparing across languages. (Early WIP)
|
36
36
|
|
37
|
-
|
38
37
|
### Supported Formats
|
39
38
|
|
40
|
-
- `
|
39
|
+
- `swift-interface`: Swift module interface files like they are created for frameworks (with library evolution support turned on..?).
|
41
40
|
- `kotlin-bcv`: The output of [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator).
|
42
41
|
|
42
|
+
### Limitations
|
43
|
+
|
44
|
+
- `kotlin-bcv` currently does not support nullability.
|
45
|
+
|
43
46
|
## Development
|
44
47
|
|
45
48
|
After checking out the repo, run `bin/setup` to install dependencies.
|
data/lib/api_diff/api.rb
CHANGED
@@ -8,19 +8,27 @@ module ApiDiff
|
|
8
8
|
@enums = []
|
9
9
|
end
|
10
10
|
|
11
|
-
def class(named:)
|
12
|
-
classes.find { |c| c.name == named }
|
11
|
+
def class(named: nil, fully_qualified_name: nil)
|
12
|
+
classes.find { |c| c.name == named || c.fully_qualified_name == fully_qualified_name }
|
13
13
|
end
|
14
14
|
|
15
|
-
def interface(named:)
|
16
|
-
interfaces.find { |i| i.name == named }
|
15
|
+
def interface(named: nil, fully_qualified_name: nil)
|
16
|
+
interfaces.find { |i| i.name == named || i.fully_qualified_name == fully_qualified_name }
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
19
|
+
def enum(named: nil, fully_qualified_name: nil)
|
20
|
+
enums.find { |e| e.name == named || e.fully_qualified_name == fully_qualified_name }
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s(fully_qualified_names: true, global_sort: false)
|
20
24
|
result = []
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
if global_sort
|
26
|
+
result << (enums + interfaces + classes).sort.map { |e| e.to_s(fully_qualified_name: fully_qualified_names) }
|
27
|
+
else
|
28
|
+
result << enums.sort.map { |e| e.to_s(fully_qualified_name: fully_qualified_names) }
|
29
|
+
result << interfaces.sort.map { |i| i.to_s(fully_qualified_name: fully_qualified_names) }
|
30
|
+
result << classes.sort.map { |c| c.to_s(fully_qualified_name: fully_qualified_names) }
|
31
|
+
end
|
24
32
|
result.flatten.join("\n\n")
|
25
33
|
end
|
26
34
|
end
|
data/lib/api_diff/cli.rb
CHANGED
@@ -5,8 +5,9 @@ module ApiDiff
|
|
5
5
|
def parse(arguments)
|
6
6
|
parser = OptionParser.new do |opts|
|
7
7
|
opts.on("-f", "--format FORMAT", ["swift-interface", "kotlin-bcv"])
|
8
|
-
opts.on("-s", "--
|
8
|
+
opts.on("-s", "--short-names", "Use short instead of fully qualified names")
|
9
9
|
opts.on("-n", "--normalize")
|
10
|
+
opts.on("-g", "--global-sort")
|
10
11
|
end
|
11
12
|
|
12
13
|
options = {}
|
@@ -34,8 +35,12 @@ module ApiDiff
|
|
34
35
|
parser = KotlinBCVParser.new(options)
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
parser.parse(IO.read(options[:input]))
|
39
|
+
output = parser.api.to_s(
|
40
|
+
fully_qualified_names: !options[:"short-names"],
|
41
|
+
global_sort: options[:"global-sort"]
|
42
|
+
)
|
43
|
+
puts output
|
39
44
|
end
|
40
45
|
end
|
41
46
|
end
|
data/lib/api_diff/interface.rb
CHANGED
@@ -2,55 +2,61 @@ module ApiDiff
|
|
2
2
|
# Biggest Drawback: Does not support optionals :-/
|
3
3
|
class KotlinBCVParser < Parser
|
4
4
|
def parse(content)
|
5
|
-
Property.
|
6
|
-
Property.readonly_keyword = "val"
|
7
|
-
|
8
|
-
api = Api.new
|
5
|
+
Property.readonly_keyword = "val" unless @options[:normalize]
|
9
6
|
|
10
7
|
sections = content.scan(/^.+?{$.*?^}$/m)
|
11
8
|
sections.each do |section|
|
12
9
|
section.strip!
|
13
10
|
first_line = section.split("\n")[0]
|
14
11
|
if first_line.include?(" : java/lang/Enum")
|
15
|
-
|
12
|
+
parse_enum(section)
|
13
|
+
elsif first_line.include?("interface class")
|
14
|
+
parse_interface(section)
|
16
15
|
elsif first_line.match?(/public.+class/)
|
17
|
-
|
18
|
-
# elsif first_line.match?(/public protocol/)
|
19
|
-
# api.interfaces << parse_interface(section)
|
20
|
-
# elsif first_line.match?(/extension/)
|
21
|
-
# parse_extension(api, section)
|
16
|
+
parse_class(section)
|
22
17
|
end
|
23
18
|
end
|
24
19
|
|
25
20
|
normalize!(api) if @options[:normalize]
|
26
|
-
|
27
|
-
api
|
28
21
|
end
|
29
22
|
|
30
23
|
private
|
31
24
|
|
32
25
|
def parse_class(class_content)
|
33
|
-
|
34
|
-
|
26
|
+
qualified_name = transform_package_path class_content.match(/public.+class ([^\s]+)/)[1]
|
27
|
+
|
28
|
+
cls = Class.new(unqualify(qualified_name), qualified_name)
|
35
29
|
cls.parents = parse_parents(class_content)
|
36
30
|
cls.functions = parse_functions(class_content)
|
37
31
|
extract_properties(cls)
|
38
|
-
cls
|
32
|
+
api.classes << cls
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_interface(interface_content)
|
36
|
+
qualified_name = transform_package_path interface_content.match(/public.+class ([^\s]+)/)[1]
|
37
|
+
|
38
|
+
interface = Interface.new(unqualify(qualified_name), qualified_name)
|
39
|
+
interface.parents = parse_parents(interface_content)
|
40
|
+
interface.functions = parse_functions(interface_content)
|
41
|
+
extract_properties(interface)
|
42
|
+
|
43
|
+
api.interfaces << interface
|
39
44
|
end
|
40
45
|
|
41
46
|
def parse_enum(enum_content)
|
42
|
-
|
43
|
-
|
47
|
+
qualified_name = transform_package_path enum_content.match(/public.+class ([^\s]+)/)[1]
|
48
|
+
|
49
|
+
enum = Enum.new(unqualify(qualified_name), qualified_name)
|
44
50
|
enum.cases = parse_enum_cases(enum_content)
|
45
51
|
enum.functions = parse_functions(enum_content)
|
46
52
|
extract_properties(enum)
|
47
|
-
enum
|
53
|
+
api.enums << enum
|
48
54
|
end
|
49
55
|
|
50
56
|
def parse_parents(content)
|
51
57
|
parents_match = content.match(/\A.+?: (.+?) \{$/)
|
52
58
|
return [] if parents_match.nil?
|
53
|
-
parents_match[1].split(",").map { |p| transform_package_path(p.strip) }
|
59
|
+
parents_match[1].split(",").map { |p| unqualify(transform_package_path(p.strip)) }
|
54
60
|
end
|
55
61
|
|
56
62
|
def parse_functions(content)
|
@@ -59,12 +65,12 @@ module ApiDiff
|
|
59
65
|
next if match[:name]&.start_with? "component" # don't add data class `componentX` methods
|
60
66
|
params_range = ((match.begin(:params) - match.begin(:signature))...(match.end(:params) - match.begin(:signature)))
|
61
67
|
signature = match[:signature]
|
62
|
-
signature[params_range] =
|
68
|
+
signature[params_range] = map_jvm_types(match[:params]).join(", ")
|
63
69
|
signature.gsub!(/synthetic ?/, "") # synthetic or not, it's part of the API
|
64
70
|
Function.new(
|
65
71
|
name: (match[:name] || match[:init]),
|
66
72
|
signature: signature,
|
67
|
-
return_type: match[:init].nil? ?
|
73
|
+
return_type: match[:init].nil? ? map_jvm_types(match[:return_type]).join : nil,
|
68
74
|
static: !match[:static].nil?,
|
69
75
|
constructor: (not match[:init].nil?)
|
70
76
|
)
|
@@ -103,10 +109,10 @@ module ApiDiff
|
|
103
109
|
end
|
104
110
|
|
105
111
|
def transform_package_path(path)
|
106
|
-
|
112
|
+
path.gsub("/", ".")
|
107
113
|
end
|
108
114
|
|
109
|
-
def
|
115
|
+
def map_jvm_types(types)
|
110
116
|
mapping = {
|
111
117
|
"Z" => "Boolean",
|
112
118
|
"B" => "Byte",
|
@@ -121,7 +127,7 @@ module ApiDiff
|
|
121
127
|
vm_types_regexp = /(?<array>\[)?(?<type>Z|B|C|S|I|J|F|D|V|(L(?<class>[^;]+);))/
|
122
128
|
all_matches(types, vm_types_regexp).map do |match|
|
123
129
|
if match[:class]
|
124
|
-
result = transform_package_path
|
130
|
+
result = unqualify(transform_package_path(match[:class]))
|
125
131
|
else
|
126
132
|
result = mapping[match[:type]]
|
127
133
|
end
|
@@ -131,13 +137,14 @@ module ApiDiff
|
|
131
137
|
end
|
132
138
|
|
133
139
|
def normalize!(api)
|
134
|
-
|
140
|
+
api.classes.reject! { |c| c.parents == ["Factory"] }
|
141
|
+
api.classes.reject! { |c| c.name.start_with? "Dagger" }
|
135
142
|
|
136
143
|
# remove abstract & final
|
137
144
|
# fun -> func
|
138
145
|
# <init> -> init
|
139
146
|
# remove space before (
|
140
|
-
(api.classes + api.enums).flat_map(&:functions).each do |f|
|
147
|
+
(api.classes + api.interfaces + api.enums).flat_map(&:functions).each do |f|
|
141
148
|
f.signature.gsub!(/(?:abstract )?(?:final )?fun (<?\w+>?) \(/, "func \\1(")
|
142
149
|
f.signature.gsub!("func <init>", "init")
|
143
150
|
end
|
data/lib/api_diff/parser.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
module ApiDiff
|
2
2
|
class Parser
|
3
|
+
attr_reader :api
|
4
|
+
|
3
5
|
def initialize(options = {})
|
4
6
|
@options = options
|
7
|
+
@api = Api.new
|
5
8
|
end
|
6
9
|
|
7
10
|
protected
|
@@ -11,8 +14,8 @@ module ApiDiff
|
|
11
14
|
string.to_enum(:scan, regex).map { Regexp.last_match }
|
12
15
|
end
|
13
16
|
|
14
|
-
def
|
15
|
-
return definition unless @options[:"
|
17
|
+
def unqualify(definition)
|
18
|
+
return definition unless @options[:"short-names"]
|
16
19
|
definition&.gsub(/(?:\w+\.){1,}(\w+)/, "\\1")
|
17
20
|
end
|
18
21
|
end
|
@@ -1,69 +1,101 @@
|
|
1
1
|
module ApiDiff
|
2
2
|
class SwiftInterfaceParser < Parser
|
3
3
|
def parse(content)
|
4
|
-
|
4
|
+
Interface.type_name = "protocol" unless @options[:normalize]
|
5
5
|
|
6
|
-
|
6
|
+
module_name_match = content.match(/@_exported import (\w+)/)
|
7
|
+
container_types = module_name_match ? module_name_match[1] : []
|
8
|
+
parse_blocks(content, container_types)
|
9
|
+
parse_one_line_extensions(content)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def parse_blocks(content, container_types = [])
|
15
|
+
sections = content.scan(/^[^\n]+?{$.*?^}$/m)
|
7
16
|
sections.each do |section|
|
8
17
|
first_line = section.split("\n")[0]
|
9
|
-
if first_line.
|
10
|
-
|
11
|
-
elsif first_line.
|
12
|
-
|
13
|
-
elsif first_line.
|
14
|
-
parse_extension(
|
15
|
-
elsif first_line.
|
16
|
-
|
18
|
+
if first_line.include? "public class"
|
19
|
+
parse_class(section, container_types)
|
20
|
+
elsif first_line.include? "public protocol"
|
21
|
+
parse_ptotocol(section, container_types)
|
22
|
+
elsif first_line.include? "extension"
|
23
|
+
parse_extension(section, container_types)
|
24
|
+
elsif first_line.include? "public enum"
|
25
|
+
parse_enum(section, container_types)
|
17
26
|
end
|
18
27
|
end
|
19
|
-
|
20
|
-
api
|
21
28
|
end
|
22
29
|
|
23
|
-
|
30
|
+
def parse_one_line_extensions(content)
|
31
|
+
one_line_extensions = content.scan(/^extension.+\{\}$/)
|
32
|
+
one_line_extensions.each do |extension|
|
33
|
+
parse_extension(extension, [])
|
34
|
+
end
|
35
|
+
end
|
24
36
|
|
25
|
-
def parse_class(class_content)
|
37
|
+
def parse_class(class_content, container_types)
|
26
38
|
name = class_content.match(/public class (\w+)/)[1]
|
27
|
-
|
39
|
+
|
40
|
+
cls = Class.new(name, qualified_name(name, container_types))
|
28
41
|
cls.parents = parse_parents(class_content)
|
29
42
|
cls.properties = parse_properties(class_content)
|
30
43
|
cls.functions = parse_functions(class_content)
|
31
|
-
cls
|
44
|
+
api.classes << cls
|
45
|
+
|
46
|
+
parse_nested_types(class_content, [*container_types, name])
|
32
47
|
end
|
33
48
|
|
34
|
-
def
|
35
|
-
name =
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
49
|
+
def parse_ptotocol(protocol_content, container_types)
|
50
|
+
name = protocol_content.match(/public protocol (\w+)/)[1]
|
51
|
+
|
52
|
+
protocol = Interface.new(name, qualified_name(name, container_types))
|
53
|
+
protocol.parents = parse_parents(protocol_content)
|
54
|
+
protocol.properties = parse_properties(protocol_content)
|
55
|
+
protocol.functions = parse_functions(protocol_content)
|
56
|
+
api.interfaces << protocol
|
57
|
+
|
58
|
+
parse_nested_types(protocol_content, [*container_types, name])
|
41
59
|
end
|
42
60
|
|
43
|
-
def parse_extension(
|
44
|
-
name = content.match(/extension (\w+)/)[1]
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
61
|
+
def parse_extension(content, container_types)
|
62
|
+
name = content.match(/extension ([\w\.]+)/)[1]
|
63
|
+
|
64
|
+
base_type = api.class(fully_qualified_name: qualified_name(name, container_types))
|
65
|
+
base_type ||= api.interface(fully_qualified_name: qualified_name(name, container_types))
|
66
|
+
base_type ||= api.enum(fully_qualified_name: qualified_name(name, container_types))
|
67
|
+
raise Error.new "Unable to find base type for extension `#{name}`" if base_type.nil?
|
68
|
+
base_type.parents.append(*parse_parents(content)).uniq!
|
69
|
+
base_type.properties.append(*parse_properties(content)).uniq!
|
70
|
+
base_type.functions.append(*parse_functions(content)).uniq!
|
71
|
+
|
72
|
+
parse_nested_types(content, [*container_types, name])
|
51
73
|
end
|
52
74
|
|
53
|
-
def parse_enum(enum_content)
|
75
|
+
def parse_enum(enum_content, container_types)
|
54
76
|
name = enum_content.match(/public enum (\w+)/)[1]
|
55
|
-
|
77
|
+
|
78
|
+
enum = Enum.new(name, qualified_name(name, container_types))
|
56
79
|
enum.cases = parse_enum_cases(enum_content)
|
57
80
|
enum.parents = parse_parents(enum_content)
|
58
81
|
enum.properties = parse_properties(enum_content)
|
59
82
|
enum.functions = parse_functions(enum_content)
|
60
|
-
enum
|
83
|
+
api.enums << enum
|
84
|
+
|
85
|
+
parse_nested_types(enum_content, [*container_types, name])
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_nested_types(outer_content, container_types)
|
89
|
+
# remove first and last line and un-indent the inner lines
|
90
|
+
inner_content = outer_content.split("\n")[1..-2].map { |l| l.gsub(/^\s{,2}/, "") }.join("\n")
|
91
|
+
|
92
|
+
parse_blocks(inner_content, container_types)
|
61
93
|
end
|
62
94
|
|
63
95
|
def parse_parents(content)
|
64
|
-
parents_match = content.match(/\A.+?: (.+?) \{
|
96
|
+
parents_match = content.match(/\A.+?: (.+?) \{\}?$/)
|
65
97
|
return [] if parents_match.nil?
|
66
|
-
parents_match[1].split(",").map { |p|
|
98
|
+
parents_match[1].split(",").map { |p| unqualify(p.strip) }
|
67
99
|
end
|
68
100
|
|
69
101
|
def parse_properties(content)
|
@@ -71,7 +103,7 @@ module ApiDiff
|
|
71
103
|
all_matches(content, property_regexp).map do |match|
|
72
104
|
Property.new(
|
73
105
|
name: match[:name],
|
74
|
-
type:
|
106
|
+
type: unqualify(match[:type]),
|
75
107
|
writable: (match[:varlet] == "var" && (match[:get] == nil || match[:set] != nil)),
|
76
108
|
static: !match[:static].nil?
|
77
109
|
)
|
@@ -83,8 +115,8 @@ module ApiDiff
|
|
83
115
|
all_matches(content, method_regexp).map do |match|
|
84
116
|
Function.new(
|
85
117
|
name: (match[:name] || match[:init]),
|
86
|
-
signature: strip_internal_parameter_names(
|
87
|
-
return_type:
|
118
|
+
signature: strip_internal_parameter_names(unqualify(match[:signature])).strip,
|
119
|
+
return_type: unqualify(match[:return_type]),
|
88
120
|
static: !match[:static].nil?,
|
89
121
|
constructor: (not match[:init].nil?)
|
90
122
|
)
|
@@ -94,13 +126,16 @@ module ApiDiff
|
|
94
126
|
def parse_enum_cases(content)
|
95
127
|
case_regexp = /case (?<name>.+)$/
|
96
128
|
all_matches(content, case_regexp).map do |match|
|
97
|
-
match[:name]
|
129
|
+
unqualify(match[:name])
|
98
130
|
end
|
99
131
|
end
|
100
132
|
|
133
|
+
def qualified_name(name, container_types)
|
134
|
+
[*container_types, name].join(".")
|
135
|
+
end
|
136
|
+
|
101
137
|
def strip_internal_parameter_names(signature)
|
102
138
|
signature.gsub(/(\w+)\s\w+:/, "\\1:")
|
103
139
|
end
|
104
|
-
|
105
140
|
end
|
106
141
|
end
|
data/lib/api_diff/type.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
module ApiDiff
|
2
2
|
class Type
|
3
|
-
|
3
|
+
def self.type_name
|
4
|
+
name.split('::').last.downcase
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :name, :fully_qualified_name
|
4
8
|
attr_accessor :parents, :functions, :properties
|
5
9
|
|
6
|
-
def initialize(name)
|
10
|
+
def initialize(name, fully_qualified_name)
|
7
11
|
@name = name
|
12
|
+
@fully_qualified_name = fully_qualified_name
|
8
13
|
@parents = []
|
9
14
|
@functions = []
|
10
15
|
@properties = []
|
11
16
|
end
|
12
17
|
|
13
|
-
def declaration
|
14
|
-
|
15
|
-
result = "#{
|
18
|
+
def declaration(fully_qualified_name: false)
|
19
|
+
name = fully_qualified_name ? self.fully_qualified_name : self.name
|
20
|
+
result = "#{self.class.type_name} #{name}"
|
16
21
|
result += " : #{parents.join(", ")}" if has_parents?
|
17
22
|
result
|
18
23
|
end
|
@@ -28,12 +33,16 @@ module ApiDiff
|
|
28
33
|
]
|
29
34
|
end
|
30
35
|
|
31
|
-
def
|
36
|
+
def <=>(other)
|
37
|
+
name <=> other.name
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s(fully_qualified_name: true)
|
32
41
|
body = sections.map { |s| s.empty? ? nil : s }.compact # remove empty sections
|
33
42
|
body.map! { |s| s.map { |entry| " #{entry}" } } # convert every entry in every section into a string and indent it
|
34
43
|
body.map! { |s| s.join("\n") } # join all entries into a long string
|
35
44
|
[
|
36
|
-
"#{declaration} {",
|
45
|
+
"#{declaration(fully_qualified_name: fully_qualified_name)} {",
|
37
46
|
body.join("\n\n"),
|
38
47
|
"}"
|
39
48
|
].join("\n")
|
data/lib/api_diff/version.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class KotlinBCVParserTest < Minitest::Test
|
4
|
-
def
|
5
|
-
ApiDiff::KotlinBCVParser.new "
|
4
|
+
def parse(content, short_names: true, normalize: false)
|
5
|
+
parser = ApiDiff::KotlinBCVParser.new "short-names": short_names, normalize: normalize
|
6
|
+
parser.parse(content)
|
7
|
+
parser.api
|
6
8
|
end
|
7
9
|
|
8
10
|
def test_returns_api
|
9
|
-
assert_instance_of ApiDiff::Api,
|
11
|
+
assert_instance_of ApiDiff::Api, parse("")
|
10
12
|
end
|
11
13
|
|
12
14
|
def test_classes
|
@@ -23,7 +25,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
23
25
|
}
|
24
26
|
EOF
|
25
27
|
|
26
|
-
api =
|
28
|
+
api = parse(input, short_names: false)
|
27
29
|
classes = api.classes
|
28
30
|
assert_equal 5, classes.size
|
29
31
|
|
@@ -63,7 +65,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
63
65
|
}
|
64
66
|
EOF
|
65
67
|
|
66
|
-
api =
|
68
|
+
api = parse(input)
|
67
69
|
functions = api.classes.first.functions
|
68
70
|
assert_equal 9, functions.size
|
69
71
|
|
@@ -110,7 +112,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
110
112
|
}
|
111
113
|
EOF
|
112
114
|
|
113
|
-
api =
|
115
|
+
api = parse(input)
|
114
116
|
functions = api.classes.first.functions
|
115
117
|
assert_equal 0, functions.size
|
116
118
|
end
|
@@ -126,7 +128,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
126
128
|
}
|
127
129
|
EOF
|
128
130
|
|
129
|
-
api =
|
131
|
+
api = parse(input)
|
130
132
|
assert_equal 0, api.classes.first.functions.size
|
131
133
|
properties = api.classes.first.properties
|
132
134
|
assert_equal 4, properties.size
|
@@ -150,6 +152,29 @@ class KotlinBCVParserTest < Minitest::Test
|
|
150
152
|
assert_equal "fqdn", fqdn.name
|
151
153
|
end
|
152
154
|
|
155
|
+
def test_interfaces
|
156
|
+
input = <<~EOF
|
157
|
+
public abstract interface class com/package/WithFunctions {
|
158
|
+
public abstract fun abstractAction ()V
|
159
|
+
}
|
160
|
+
public abstract interface class com/nested/package/WithProperties {
|
161
|
+
public final fun getName ()Ljava/lang/String;
|
162
|
+
public final fun setName (Ljava/lang/String;)V
|
163
|
+
}
|
164
|
+
EOF
|
165
|
+
|
166
|
+
api = parse(input)
|
167
|
+
interfaces = api.interfaces
|
168
|
+
assert_equal 2, interfaces.size
|
169
|
+
|
170
|
+
with_functions = interfaces[0]
|
171
|
+
assert_equal "abstractAction", with_functions.functions[0].name
|
172
|
+
|
173
|
+
with_properties = interfaces[1]
|
174
|
+
assert_equal 1, with_properties.properties.size
|
175
|
+
assert_equal "name", with_properties.properties[0].name
|
176
|
+
end
|
177
|
+
|
153
178
|
def test_companion_objects
|
154
179
|
# TypeCode
|
155
180
|
# Metadata
|
@@ -171,7 +196,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
171
196
|
}
|
172
197
|
EOF
|
173
198
|
|
174
|
-
api =
|
199
|
+
api = parse(input)
|
175
200
|
enums = api.enums
|
176
201
|
assert_equal 1, enums.size
|
177
202
|
|
@@ -201,7 +226,7 @@ class KotlinBCVParserTest < Minitest::Test
|
|
201
226
|
}
|
202
227
|
EOF
|
203
228
|
|
204
|
-
api =
|
229
|
+
api = parse(input, normalize: true)
|
205
230
|
|
206
231
|
first_class = api.classes.first
|
207
232
|
assert_equal 3, first_class.functions.size
|
@@ -1,12 +1,14 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
3
|
class SwiftInterfaceParserTest < Minitest::Test
|
4
|
-
def
|
5
|
-
ApiDiff::SwiftInterfaceParser.new "
|
4
|
+
def parse(content)
|
5
|
+
parser = ApiDiff::SwiftInterfaceParser.new "short-names": true
|
6
|
+
parser.parse(content)
|
7
|
+
parser.api
|
6
8
|
end
|
7
9
|
|
8
10
|
def test_returns_api
|
9
|
-
assert_instance_of ApiDiff::Api,
|
11
|
+
assert_instance_of ApiDiff::Api, parse("")
|
10
12
|
end
|
11
13
|
|
12
14
|
def test_classes
|
@@ -20,7 +22,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
20
22
|
public class Fourth : Swift.Codable, Swift.Hashable {
|
21
23
|
}
|
22
24
|
EOF
|
23
|
-
api =
|
25
|
+
api = parse(input)
|
24
26
|
classes = api.classes
|
25
27
|
assert_equal 4, classes.size
|
26
28
|
|
@@ -55,7 +57,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
55
57
|
}
|
56
58
|
}
|
57
59
|
EOF
|
58
|
-
api =
|
60
|
+
api = parse(input)
|
59
61
|
properties = api.classes.first.properties
|
60
62
|
assert_equal 4, properties.size
|
61
63
|
|
@@ -93,7 +95,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
93
95
|
}
|
94
96
|
EOF
|
95
97
|
|
96
|
-
api =
|
98
|
+
api = parse(input)
|
97
99
|
functions = api.classes.first.functions
|
98
100
|
assert_equal 6, functions.size
|
99
101
|
|
@@ -144,7 +146,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
144
146
|
}
|
145
147
|
EOF
|
146
148
|
|
147
|
-
api =
|
149
|
+
api = parse(input)
|
148
150
|
classes = api.classes
|
149
151
|
assert_equal 3, classes.size
|
150
152
|
|
@@ -175,7 +177,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
175
177
|
}
|
176
178
|
EOF
|
177
179
|
|
178
|
-
api =
|
180
|
+
api = parse(input)
|
179
181
|
interfaces = api.interfaces
|
180
182
|
assert_equal 2, interfaces.size
|
181
183
|
|
@@ -216,7 +218,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
216
218
|
}
|
217
219
|
EOF
|
218
220
|
|
219
|
-
api =
|
221
|
+
api = parse(input)
|
220
222
|
interfaces = api.interfaces
|
221
223
|
assert_equal 3, interfaces.size
|
222
224
|
|
@@ -241,8 +243,9 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
241
243
|
case a
|
242
244
|
}
|
243
245
|
@frozen public enum Beta {
|
244
|
-
case c
|
245
|
-
case d
|
246
|
+
case c(number: Swift.Int)
|
247
|
+
case d(name: Swift.String? = nil)
|
248
|
+
case lambda(func: ((Swift.Double) -> Swift.Void)?)
|
246
249
|
}
|
247
250
|
@frozen public enum Gamma : Swift.String, Swift.CaseIterable {
|
248
251
|
case e
|
@@ -251,7 +254,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
251
254
|
}
|
252
255
|
EOF
|
253
256
|
|
254
|
-
api =
|
257
|
+
api = parse(input)
|
255
258
|
enums = api.enums
|
256
259
|
assert_equal 3, enums.size
|
257
260
|
|
@@ -261,7 +264,7 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
261
264
|
|
262
265
|
beta = enums[1]
|
263
266
|
assert_equal "Beta", beta.name
|
264
|
-
assert_equal ["c", "d"], beta.cases
|
267
|
+
assert_equal ["c(number: Int)", "d(name: String? = nil)", "lambda(func: ((Double) -> Void)?)"], beta.cases
|
265
268
|
|
266
269
|
gamma = enums[2]
|
267
270
|
assert_equal "Gamma", gamma.name
|
@@ -269,4 +272,130 @@ class SwiftInterfaceParserTest < Minitest::Test
|
|
269
272
|
assert_equal ["e", "f", "g"], gamma.cases
|
270
273
|
assert_equal ["String", "CaseIterable"], gamma.parents
|
271
274
|
end
|
275
|
+
|
276
|
+
def test_ignores_header_noise
|
277
|
+
input = <<~EOF
|
278
|
+
// swift-interface-format-version: 1.0
|
279
|
+
// swift-compiler-version: Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
|
280
|
+
// swift-module-flags: -target arm64-apple-ios12.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name ePA
|
281
|
+
import AVFoundation
|
282
|
+
import CoreMotion
|
283
|
+
import CoreNFC
|
284
|
+
import Foundation
|
285
|
+
import Security
|
286
|
+
import Swift
|
287
|
+
import UIKit
|
288
|
+
@_exported import MyLib
|
289
|
+
import os.log
|
290
|
+
import os
|
291
|
+
public class NotIgnored {
|
292
|
+
}
|
293
|
+
EOF
|
294
|
+
|
295
|
+
api = parse(input)
|
296
|
+
|
297
|
+
assert api.class(named: "NotIgnored")
|
298
|
+
end
|
299
|
+
|
300
|
+
def test_full_name_qualification
|
301
|
+
input = <<~EOF
|
302
|
+
@_exported import MyLib
|
303
|
+
public class Qualified {
|
304
|
+
}
|
305
|
+
EOF
|
306
|
+
|
307
|
+
api = parse(input)
|
308
|
+
|
309
|
+
qualified = api.classes.first
|
310
|
+
assert_equal "MyLib.Qualified", qualified.fully_qualified_name
|
311
|
+
end
|
312
|
+
|
313
|
+
def test_nested_types
|
314
|
+
input = <<~EOF
|
315
|
+
public class OuterClass {
|
316
|
+
public class InnerClass {
|
317
|
+
public func a()
|
318
|
+
}
|
319
|
+
}
|
320
|
+
extension OuterClass {
|
321
|
+
@frozen public enum ExtensionInner {
|
322
|
+
case ei1
|
323
|
+
}
|
324
|
+
}
|
325
|
+
public enum OuterEnum {
|
326
|
+
public class EnumInnerClass {
|
327
|
+
}
|
328
|
+
}
|
329
|
+
public enum Level1 {
|
330
|
+
public class Level2 {
|
331
|
+
public enum Level3 {
|
332
|
+
public class Level4 {
|
333
|
+
|
334
|
+
}
|
335
|
+
}
|
336
|
+
}
|
337
|
+
}
|
338
|
+
EOF
|
339
|
+
|
340
|
+
api = parse(input)
|
341
|
+
|
342
|
+
inner_class = api.class(named: "InnerClass")
|
343
|
+
assert_equal "OuterClass.InnerClass", inner_class.fully_qualified_name
|
344
|
+
assert_equal "a", inner_class.functions.first.name
|
345
|
+
|
346
|
+
extension_inner = api.enum(named: "ExtensionInner")
|
347
|
+
assert_equal ["ei1"], extension_inner.cases
|
348
|
+
assert_equal "OuterClass.ExtensionInner", extension_inner.fully_qualified_name
|
349
|
+
|
350
|
+
enum_inner_class = api.class(named: "EnumInnerClass")
|
351
|
+
assert_equal "OuterEnum.EnumInnerClass", enum_inner_class.fully_qualified_name
|
352
|
+
|
353
|
+
assert api.enum(named: "Level1")
|
354
|
+
assert api.class(named: "Level2")
|
355
|
+
assert api.enum(named: "Level3")
|
356
|
+
assert api.class(named: "Level4")
|
357
|
+
assert_equal "Level1.Level2.Level3.Level4", api.class(named: "Level4").fully_qualified_name
|
358
|
+
end
|
359
|
+
|
360
|
+
def test_nested_name_conflict
|
361
|
+
input = <<~EOF
|
362
|
+
public class Conflict {
|
363
|
+
}
|
364
|
+
public enum Nested {
|
365
|
+
public class Conflict {
|
366
|
+
}
|
367
|
+
}
|
368
|
+
extension Conflict {
|
369
|
+
public func topLevel()
|
370
|
+
}
|
371
|
+
extension Nested.Conflict {
|
372
|
+
public func nested()
|
373
|
+
}
|
374
|
+
EOF
|
375
|
+
|
376
|
+
api = parse(input)
|
377
|
+
|
378
|
+
assert_equal 2, api.classes.size
|
379
|
+
|
380
|
+
top_level = api.class(fully_qualified_name: "Conflict")
|
381
|
+
assert_equal "topLevel", top_level.functions.first.name
|
382
|
+
|
383
|
+
nested = api.class(fully_qualified_name: "Nested.Conflict")
|
384
|
+
assert_equal "nested", nested.functions.first.name
|
385
|
+
end
|
386
|
+
|
387
|
+
def test_qualified_one_line_extensions
|
388
|
+
input = <<~EOF
|
389
|
+
@_exported import MyLib
|
390
|
+
public enum Enum {
|
391
|
+
case a
|
392
|
+
}
|
393
|
+
extension MyLib.Enum : Swift.Hashable {}
|
394
|
+
EOF
|
395
|
+
|
396
|
+
api = parse(input)
|
397
|
+
enum = api.enums.first
|
398
|
+
|
399
|
+
assert_equal ["Hashable"], enum.parents
|
400
|
+
end
|
272
401
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_diff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Ludwig
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|