api_diff 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +58 -0
- data/exe/api_diff +9 -0
- data/lib/api_diff.rb +16 -0
- data/lib/api_diff/api.rb +27 -0
- data/lib/api_diff/class.rb +4 -0
- data/lib/api_diff/cli.rb +41 -0
- data/lib/api_diff/enum.rb +12 -0
- data/lib/api_diff/function.rb +58 -0
- data/lib/api_diff/interface.rb +4 -0
- data/lib/api_diff/kotlin_bcv_parser.rb +157 -0
- data/lib/api_diff/parser.rb +19 -0
- data/lib/api_diff/property.rb +52 -0
- data/lib/api_diff/swift_interface_parser.rb +106 -0
- data/lib/api_diff/type.rb +42 -0
- data/lib/api_diff/version.rb +3 -0
- data/test/kotlin_bcv_parser_test.rb +215 -0
- data/test/swift_interface_parser_test.rb +272 -0
- data/test/test_helper.rb +5 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bcfbf61d2a4f9b1a5abfaf3ee30e6c41a12048e971281424797bfc8067c9d467
|
4
|
+
data.tar.gz: aabe1cc6429ff84100719bf26d691e9825502ffc2bf32736a9880c4f308477c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd3944386bbdf2c713b2ac647dc38fc50132211ee9af2f06c97f821e31aca8981e710b5a9c2bbc987b8866ed512dc09ad9f2ed802f6bfcd98e33fd2d2858d99d
|
7
|
+
data.tar.gz: bbd34214c44954e6f2075fbd26a9115a0cbad590790565c282447b67e6590b8a7533678558f7ca3e7b995d11367df8ee91be131532b3da9fc41dcae954854020
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Sebastian Ludwig
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# API Diff
|
2
|
+
|
3
|
+
Bring APIs into an easily diff-able format.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's `Gemfile`
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'api_diff'
|
11
|
+
```
|
12
|
+
|
13
|
+
and then execute
|
14
|
+
|
15
|
+
```bash
|
16
|
+
bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install api_diff
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
```bash
|
28
|
+
./api_diff --format FORMAT <input-file>
|
29
|
+
```
|
30
|
+
|
31
|
+
All output is printed to `STDOUT`.
|
32
|
+
|
33
|
+
- `--format`: required - specifies the format of the input file (see below).
|
34
|
+
- `--strip-packages`: optional - shorten type references by removing package qualifiers.
|
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
|
+
|
37
|
+
|
38
|
+
### Supported Formats
|
39
|
+
|
40
|
+
- `swiftinterface`: Swift module interface files like they are created for frameworks (with library evolution support turned on..?).
|
41
|
+
- `kotlin-bcv`: The output of [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator).
|
42
|
+
|
43
|
+
## Development
|
44
|
+
|
45
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
46
|
+
Then, run `rake test` to run the tests.
|
47
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
48
|
+
|
49
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
50
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
51
|
+
|
52
|
+
## Contributing
|
53
|
+
|
54
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sebastianludwig/api-diff.
|
55
|
+
|
56
|
+
## License
|
57
|
+
|
58
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/api_diff
ADDED
data/lib/api_diff.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "api_diff/version"
|
2
|
+
require "api_diff/api"
|
3
|
+
require "api_diff/type"
|
4
|
+
require "api_diff/class"
|
5
|
+
require "api_diff/interface"
|
6
|
+
require "api_diff/enum"
|
7
|
+
require "api_diff/function"
|
8
|
+
require "api_diff/property"
|
9
|
+
require "api_diff/parser"
|
10
|
+
require "api_diff/swift_interface_parser"
|
11
|
+
require "api_diff/kotlin_bcv_parser"
|
12
|
+
require "api_diff/cli"
|
13
|
+
|
14
|
+
module ApiDiff
|
15
|
+
class Error < StandardError; end
|
16
|
+
end
|
data/lib/api_diff/api.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class Api
|
3
|
+
attr_accessor :classes, :interfaces, :enums
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@classes = []
|
7
|
+
@interfaces = []
|
8
|
+
@enums = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def class(named:)
|
12
|
+
classes.find { |c| c.name == named }
|
13
|
+
end
|
14
|
+
|
15
|
+
def interface(named:)
|
16
|
+
interfaces.find { |i| i.name == named }
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
result = []
|
21
|
+
result << enums.map(&:to_s)
|
22
|
+
result << classes.map(&:to_s)
|
23
|
+
result << interfaces.map(&:to_s)
|
24
|
+
result.flatten.join("\n\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/api_diff/cli.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module ApiDiff
|
4
|
+
class Cli
|
5
|
+
def parse(arguments)
|
6
|
+
parser = OptionParser.new do |opts|
|
7
|
+
opts.on("-f", "--format FORMAT", ["swift-interface", "kotlin-bcv"])
|
8
|
+
opts.on("-s", "--strip-packages")
|
9
|
+
opts.on("-n", "--normalize")
|
10
|
+
end
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
begin
|
14
|
+
parser.parse!(arguments, into: options)
|
15
|
+
rescue OptionParser::ParseError => e
|
16
|
+
raise Error.new e.message
|
17
|
+
end
|
18
|
+
|
19
|
+
options[:input] = arguments.pop
|
20
|
+
|
21
|
+
raise Error.new "Missing argument: format" if options[:format].nil?
|
22
|
+
raise Error.new "Missing argument: input" if options[:input].nil?
|
23
|
+
|
24
|
+
options
|
25
|
+
end
|
26
|
+
|
27
|
+
def run!(arguments)
|
28
|
+
options = parse(arguments)
|
29
|
+
raise Error.new "Input file not found: #{options[:input]}" if not File.exist? options[:input]
|
30
|
+
|
31
|
+
if options[:format] == "swift-interface"
|
32
|
+
parser = SwiftInterfaceParser.new(options)
|
33
|
+
elsif options[:format] == "kotlin-bcv"
|
34
|
+
parser = KotlinBCVParser.new(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
api = parser.parse(IO.read(options[:input]))
|
38
|
+
puts api.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class Function
|
3
|
+
attr_reader :name, :signature, :return_type
|
4
|
+
|
5
|
+
def initialize(name:, signature:, return_type:, static:, constructor:)
|
6
|
+
@name = name
|
7
|
+
@signature = signature
|
8
|
+
@return_type = return_type
|
9
|
+
@static = static
|
10
|
+
@constructor = constructor
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_constructor?
|
14
|
+
@constructor
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_static?
|
18
|
+
@static
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
full_signature
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_signature
|
26
|
+
if return_type.nil?
|
27
|
+
signature
|
28
|
+
else
|
29
|
+
"#{signature} -> #{return_type}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
full_signature.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
full_signature == other.full_signature
|
39
|
+
end
|
40
|
+
|
41
|
+
def <=>(other)
|
42
|
+
# static at the bottom
|
43
|
+
return 1 if is_static? and not other.is_static?
|
44
|
+
return -1 if not is_static? and other.is_static?
|
45
|
+
|
46
|
+
# constructors first
|
47
|
+
return -1 if is_constructor? and not other.is_constructor?
|
48
|
+
return 1 if not is_constructor? and other.is_constructor?
|
49
|
+
|
50
|
+
if is_constructor?
|
51
|
+
# sort constructors by length
|
52
|
+
[full_signature.size, full_signature] <=> [other.full_signature.size, other.full_signature]
|
53
|
+
else
|
54
|
+
[name, full_signature] <=> [other.name, other.full_signature]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
# Biggest Drawback: Does not support optionals :-/
|
3
|
+
class KotlinBCVParser < Parser
|
4
|
+
def parse(content)
|
5
|
+
Property.writable_keyword = "var"
|
6
|
+
Property.readonly_keyword = "val"
|
7
|
+
|
8
|
+
api = Api.new
|
9
|
+
|
10
|
+
sections = content.scan(/^.+?{$.*?^}$/m)
|
11
|
+
sections.each do |section|
|
12
|
+
section.strip!
|
13
|
+
first_line = section.split("\n")[0]
|
14
|
+
if first_line.include?(" : java/lang/Enum")
|
15
|
+
api.enums << parse_enum(section)
|
16
|
+
elsif first_line.match?(/public.+class/)
|
17
|
+
api.classes << parse_class(section)
|
18
|
+
# elsif first_line.match?(/public protocol/)
|
19
|
+
# api.interfaces << parse_interface(section)
|
20
|
+
# elsif first_line.match?(/extension/)
|
21
|
+
# parse_extension(api, section)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
normalize!(api) if @options[:normalize]
|
26
|
+
|
27
|
+
api
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_class(class_content)
|
33
|
+
name = class_content.match(/public.+class ([^\s]+)/)[1]
|
34
|
+
cls = Class.new(transform_package_path(name))
|
35
|
+
cls.parents = parse_parents(class_content)
|
36
|
+
cls.functions = parse_functions(class_content)
|
37
|
+
extract_properties(cls)
|
38
|
+
cls
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_enum(enum_content)
|
42
|
+
name = enum_content.match(/public.+class ([^\s]+)/)[1]
|
43
|
+
enum = Enum.new(transform_package_path(name))
|
44
|
+
enum.cases = parse_enum_cases(enum_content)
|
45
|
+
enum.functions = parse_functions(enum_content)
|
46
|
+
extract_properties(enum)
|
47
|
+
enum
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_parents(content)
|
51
|
+
parents_match = content.match(/\A.+?: (.+?) \{$/)
|
52
|
+
return [] if parents_match.nil?
|
53
|
+
parents_match[1].split(",").map { |p| transform_package_path(p.strip) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_functions(content)
|
57
|
+
method_regexp = /public (?<signature>(?<static>static )?.*fun (?:(<(?<init>init)>)|(?<name>[^\s]+)) \((?<params>.*)\))(?<return_type>.+)$/
|
58
|
+
all_matches(content, method_regexp).map do |match|
|
59
|
+
next if match[:name]&.start_with? "component" # don't add data class `componentX` methods
|
60
|
+
params_range = ((match.begin(:params) - match.begin(:signature))...(match.end(:params) - match.begin(:signature)))
|
61
|
+
signature = match[:signature]
|
62
|
+
signature[params_range] = map_vm_types(match[:params]).join(", ")
|
63
|
+
signature.gsub!(/synthetic ?/, "") # synthetic or not, it's part of the API
|
64
|
+
Function.new(
|
65
|
+
name: (match[:name] || match[:init]),
|
66
|
+
signature: signature,
|
67
|
+
return_type: match[:init].nil? ? map_vm_types(match[:return_type]).join : nil,
|
68
|
+
static: !match[:static].nil?,
|
69
|
+
constructor: (not match[:init].nil?)
|
70
|
+
)
|
71
|
+
end.compact
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_enum_cases(content)
|
75
|
+
case_regexp = /public static final field (?<name>[A-Z_0-9]+)/
|
76
|
+
all_matches(content, case_regexp).map do |match|
|
77
|
+
match[:name]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_properties(type)
|
82
|
+
getters = type.functions.select { |f| f.signature.match(/fun get[A-Z](\w+)? \(\)/) }
|
83
|
+
getters.each do |getter|
|
84
|
+
setter_name = getter.name.gsub(/^get/, "set")
|
85
|
+
setter = type.functions.find { |f| f.signature.match(/fun #{setter_name} \(#{getter.return_type}\)/) }
|
86
|
+
|
87
|
+
type.functions.delete getter
|
88
|
+
type.functions.delete setter if setter
|
89
|
+
|
90
|
+
name = getter.name.gsub(/^get/, "")
|
91
|
+
if name == name.upcase # complete uppercase -> complete lowercase
|
92
|
+
name.downcase!
|
93
|
+
else
|
94
|
+
name[0] = name[0].downcase
|
95
|
+
end
|
96
|
+
type.properties << Property.new(
|
97
|
+
name: name,
|
98
|
+
type: getter.return_type,
|
99
|
+
writable: (setter != nil),
|
100
|
+
static: getter.is_static?
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def transform_package_path(path)
|
106
|
+
strip_packages(path.gsub("/", "."))
|
107
|
+
end
|
108
|
+
|
109
|
+
def map_vm_types(types)
|
110
|
+
mapping = {
|
111
|
+
"Z" => "Boolean",
|
112
|
+
"B" => "Byte",
|
113
|
+
"C" => "Char",
|
114
|
+
"S" => "Short",
|
115
|
+
"I" => "Int",
|
116
|
+
"J" => "Long",
|
117
|
+
"F" => "Float",
|
118
|
+
"D" => "Double",
|
119
|
+
"V" => "Void"
|
120
|
+
}
|
121
|
+
vm_types_regexp = /(?<array>\[)?(?<type>Z|B|C|S|I|J|F|D|V|(L(?<class>[^;]+);))/
|
122
|
+
all_matches(types, vm_types_regexp).map do |match|
|
123
|
+
if match[:class]
|
124
|
+
result = transform_package_path match[:class]
|
125
|
+
else
|
126
|
+
result = mapping[match[:type]]
|
127
|
+
end
|
128
|
+
result = "[#{result}]" if match[:array]
|
129
|
+
result
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def normalize!(api)
|
134
|
+
Property.readonly_keyword = "let"
|
135
|
+
|
136
|
+
# remove abstract & final
|
137
|
+
# fun -> func
|
138
|
+
# <init> -> init
|
139
|
+
# remove space before (
|
140
|
+
(api.classes + api.enums).flat_map(&:functions).each do |f|
|
141
|
+
f.signature.gsub!(/(?:abstract )?(?:final )?fun (<?\w+>?) \(/, "func \\1(")
|
142
|
+
f.signature.gsub!("func <init>", "init")
|
143
|
+
end
|
144
|
+
|
145
|
+
# enum screaming case -> camel case
|
146
|
+
api.enums.each do |e|
|
147
|
+
e.cases = e.cases.map do |c|
|
148
|
+
# supports double _ by preserving one of them
|
149
|
+
c.scan(/_?[A-Z0-9]+_?/).map.with_index do |p, index|
|
150
|
+
p.gsub(/_$/, "").downcase.gsub(/^_?\w/) { |m| index == 0 ? m : m.upcase }
|
151
|
+
end.join
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class Parser
|
3
|
+
def initialize(options = {})
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def all_matches(string, regex)
|
10
|
+
# taken from https://stackoverflow.com/a/6807722/588314
|
11
|
+
string.to_enum(:scan, regex).map { Regexp.last_match }
|
12
|
+
end
|
13
|
+
|
14
|
+
def strip_packages(definition)
|
15
|
+
return definition unless @options[:"strip-packages"]
|
16
|
+
definition&.gsub(/(?:\w+\.){1,}(\w+)/, "\\1")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class Property
|
3
|
+
@writable_keyword = "var"
|
4
|
+
@readonly_keyword = "let"
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_accessor :writable_keyword, :readonly_keyword
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :name, :type
|
11
|
+
|
12
|
+
def initialize(name:, type:, writable:, static:)
|
13
|
+
@name = name
|
14
|
+
@type = type
|
15
|
+
@writable = writable
|
16
|
+
@static = static
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_writable?
|
20
|
+
@writable
|
21
|
+
end
|
22
|
+
|
23
|
+
def is_static?
|
24
|
+
@static
|
25
|
+
end
|
26
|
+
|
27
|
+
def hash
|
28
|
+
to_s.hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(other)
|
32
|
+
to_s == other.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
result = []
|
37
|
+
result << "static" if is_static?
|
38
|
+
result << (is_writable? ? self.class.writable_keyword : self.class.readonly_keyword)
|
39
|
+
result << "#{name}: #{type}"
|
40
|
+
result.join(" ")
|
41
|
+
end
|
42
|
+
|
43
|
+
def <=>(other)
|
44
|
+
# static at the bottom
|
45
|
+
return 1 if is_static? and not other.is_static?
|
46
|
+
return -1 if not is_static? and other.is_static?
|
47
|
+
|
48
|
+
# sort by name
|
49
|
+
name <=> other.name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class SwiftInterfaceParser < Parser
|
3
|
+
def parse(content)
|
4
|
+
api = Api.new
|
5
|
+
|
6
|
+
sections = content.scan(/^.+?{$.*?^}$/m)
|
7
|
+
sections.each do |section|
|
8
|
+
first_line = section.split("\n")[0]
|
9
|
+
if first_line.match?(/public class/)
|
10
|
+
api.classes << parse_class(section)
|
11
|
+
elsif first_line.match?(/public protocol/)
|
12
|
+
api.interfaces << parse_interface(section)
|
13
|
+
elsif first_line.match?(/extension/)
|
14
|
+
parse_extension(api, section)
|
15
|
+
elsif first_line.match?(/public enum/)
|
16
|
+
api.enums << parse_enum(section)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
api
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_class(class_content)
|
26
|
+
name = class_content.match(/public class (\w+)/)[1]
|
27
|
+
cls = Class.new(name)
|
28
|
+
cls.parents = parse_parents(class_content)
|
29
|
+
cls.properties = parse_properties(class_content)
|
30
|
+
cls.functions = parse_functions(class_content)
|
31
|
+
cls
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_interface(interface_content)
|
35
|
+
name = interface_content.match(/public protocol (\w+)/)[1]
|
36
|
+
interface = Interface.new(name)
|
37
|
+
interface.parents = parse_parents(interface_content)
|
38
|
+
interface.properties = parse_properties(interface_content)
|
39
|
+
interface.functions = parse_functions(interface_content)
|
40
|
+
interface
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_extension(api, content)
|
44
|
+
name = content.match(/extension (\w+)/)[1]
|
45
|
+
cls = api.class(named: name)
|
46
|
+
cls ||= api.interface(named: name)
|
47
|
+
raise Error.new "Unable to find base type for extension `#{name}`" if cls.nil?
|
48
|
+
cls.parents.append(*parse_parents(content)).uniq!
|
49
|
+
cls.properties.append(*parse_properties(content)).uniq!
|
50
|
+
cls.functions.append(*parse_functions(content)).uniq!
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_enum(enum_content)
|
54
|
+
name = enum_content.match(/public enum (\w+)/)[1]
|
55
|
+
enum = Enum.new(name)
|
56
|
+
enum.cases = parse_enum_cases(enum_content)
|
57
|
+
enum.parents = parse_parents(enum_content)
|
58
|
+
enum.properties = parse_properties(enum_content)
|
59
|
+
enum.functions = parse_functions(enum_content)
|
60
|
+
enum
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_parents(content)
|
64
|
+
parents_match = content.match(/\A.+?: (.+?) \{$/)
|
65
|
+
return [] if parents_match.nil?
|
66
|
+
parents_match[1].split(",").map { |p| strip_packages(p.strip) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_properties(content)
|
70
|
+
property_regexp = /(public )?(?<static>static )?(?<varlet>var|let) (?<name>\w+): (?<type>[^\s]+)( {\s+(?<get>get)?\s+(?<set>set)?\s*})?/m
|
71
|
+
all_matches(content, property_regexp).map do |match|
|
72
|
+
Property.new(
|
73
|
+
name: match[:name],
|
74
|
+
type: strip_packages(match[:type]),
|
75
|
+
writable: (match[:varlet] == "var" && (match[:get] == nil || match[:set] != nil)),
|
76
|
+
static: !match[:static].nil?
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_functions(content)
|
82
|
+
method_regexp = /(?<signature>(?<static>static )?((func (?<name>[^\s\(]+))|(?<init>init))\s?\((?<params>.*)\).*?)(-> (?<return_type>.+))?$/
|
83
|
+
all_matches(content, method_regexp).map do |match|
|
84
|
+
Function.new(
|
85
|
+
name: (match[:name] || match[:init]),
|
86
|
+
signature: strip_internal_parameter_names(strip_packages(match[:signature])).strip,
|
87
|
+
return_type: strip_packages(match[:return_type]),
|
88
|
+
static: !match[:static].nil?,
|
89
|
+
constructor: (not match[:init].nil?)
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_enum_cases(content)
|
95
|
+
case_regexp = /case (?<name>.+)$/
|
96
|
+
all_matches(content, case_regexp).map do |match|
|
97
|
+
match[:name]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def strip_internal_parameter_names(signature)
|
102
|
+
signature.gsub(/(\w+)\s\w+:/, "\\1:")
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ApiDiff
|
2
|
+
class Type
|
3
|
+
attr_reader :name
|
4
|
+
attr_accessor :parents, :functions, :properties
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
@parents = []
|
9
|
+
@functions = []
|
10
|
+
@properties = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def declaration
|
14
|
+
kind = self.class.name.split('::').last.downcase
|
15
|
+
result = "#{kind} #{name}"
|
16
|
+
result += " : #{parents.join(", ")}" if has_parents?
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def has_parents?
|
21
|
+
not @parents.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def sections
|
25
|
+
[
|
26
|
+
properties.sort,
|
27
|
+
functions.sort
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
body = sections.map { |s| s.empty? ? nil : s }.compact # remove empty sections
|
33
|
+
body.map! { |s| s.map { |entry| " #{entry}" } } # convert every entry in every section into a string and indent it
|
34
|
+
body.map! { |s| s.join("\n") } # join all entries into a long string
|
35
|
+
[
|
36
|
+
"#{declaration} {",
|
37
|
+
body.join("\n\n"),
|
38
|
+
"}"
|
39
|
+
].join("\n")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class KotlinBCVParserTest < Minitest::Test
|
4
|
+
def parser(strip: true, normalize: false)
|
5
|
+
ApiDiff::KotlinBCVParser.new "strip-packages": strip, normalize: normalize
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_returns_api
|
9
|
+
assert_instance_of ApiDiff::Api, parser.parse("")
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_classes
|
13
|
+
input = <<~EOF
|
14
|
+
public class First {
|
15
|
+
}
|
16
|
+
public final class com/abc/Second {
|
17
|
+
}
|
18
|
+
public abstract class com/a/b/c/Third {
|
19
|
+
}
|
20
|
+
public final class com/a/Fourth : com/a/Parent {
|
21
|
+
}
|
22
|
+
public final class com/a/Fifth : com/a/Parent, java/io/Serializable {
|
23
|
+
}
|
24
|
+
EOF
|
25
|
+
|
26
|
+
api = parser(strip: false).parse(input)
|
27
|
+
classes = api.classes
|
28
|
+
assert_equal 5, classes.size
|
29
|
+
|
30
|
+
first = classes[0]
|
31
|
+
assert_equal "First", first.name
|
32
|
+
assert_equal "class First", first.declaration
|
33
|
+
|
34
|
+
second = classes[1]
|
35
|
+
assert_equal "com.abc.Second", second.name
|
36
|
+
assert_equal "class com.abc.Second", second.declaration
|
37
|
+
|
38
|
+
third = classes[2]
|
39
|
+
assert_equal "com.a.b.c.Third", third.name
|
40
|
+
assert_equal "class com.a.b.c.Third", third.declaration
|
41
|
+
|
42
|
+
fourth = classes[3]
|
43
|
+
assert_equal "com.a.Fourth", fourth.name
|
44
|
+
assert_equal "class com.a.Fourth : com.a.Parent", fourth.declaration
|
45
|
+
|
46
|
+
fifth = classes[4]
|
47
|
+
assert_equal "com.a.Fifth", fifth.name
|
48
|
+
assert_equal "class com.a.Fifth : com.a.Parent, java.io.Serializable", fifth.declaration
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_functions
|
52
|
+
input = <<~EOF
|
53
|
+
public class FirstClass {
|
54
|
+
public fun action ()V
|
55
|
+
public final fun finalAction ()V
|
56
|
+
public abstract fun abstractAction ()V
|
57
|
+
public fun hashCode ()I
|
58
|
+
public fun toString ()Ljava/lang/String;
|
59
|
+
public fun check (Ljava/lang/String;)Z
|
60
|
+
public fun <init> ()V
|
61
|
+
public synthetic fun <init> (ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
62
|
+
public static synthetic fun hide$default (Lcom/a/Second;ILjava/lang/Object;)V
|
63
|
+
}
|
64
|
+
EOF
|
65
|
+
|
66
|
+
api = parser.parse(input)
|
67
|
+
functions = api.classes.first.functions
|
68
|
+
assert_equal 9, functions.size
|
69
|
+
|
70
|
+
action = functions[0]
|
71
|
+
assert_equal "action", action.name
|
72
|
+
assert_equal "fun action () -> Void", action.full_signature
|
73
|
+
|
74
|
+
final_action = functions[1]
|
75
|
+
assert_equal "finalAction", final_action.name
|
76
|
+
assert_equal "final fun finalAction () -> Void", final_action.full_signature
|
77
|
+
|
78
|
+
abstract_action = functions[2]
|
79
|
+
assert_equal "abstractAction", abstract_action.name
|
80
|
+
assert_equal "abstract fun abstractAction () -> Void", abstract_action.full_signature
|
81
|
+
|
82
|
+
hash_code = functions[3]
|
83
|
+
assert_equal "hashCode", hash_code.name
|
84
|
+
assert_equal "fun hashCode () -> Int", hash_code.full_signature
|
85
|
+
|
86
|
+
to_string = functions[4]
|
87
|
+
assert_equal "toString", to_string.name
|
88
|
+
assert_equal "fun toString () -> String", to_string.full_signature
|
89
|
+
|
90
|
+
check = functions[5]
|
91
|
+
assert_equal "check", check.name
|
92
|
+
assert_equal "fun check (String) -> Boolean", check.full_signature
|
93
|
+
|
94
|
+
init = functions[6]
|
95
|
+
assert_equal "init", init.name
|
96
|
+
assert_equal "fun <init> ()", init.full_signature
|
97
|
+
|
98
|
+
syn_init = functions[7]
|
99
|
+
assert_equal "init", syn_init.name
|
100
|
+
assert_equal "fun <init> (Int, DefaultConstructorMarker)", syn_init.full_signature
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_omits_component_functions
|
104
|
+
input = <<~EOF
|
105
|
+
public class DataClass {
|
106
|
+
public final fun component1 ()Ljava/lang/String;
|
107
|
+
public final fun component2 ()Ljava/lang/String;
|
108
|
+
public final fun component3 ()Ljava/lang/String;
|
109
|
+
public final fun component4 ()Ljava/util/List;
|
110
|
+
}
|
111
|
+
EOF
|
112
|
+
|
113
|
+
api = parser.parse(input)
|
114
|
+
functions = api.classes.first.functions
|
115
|
+
assert_equal 0, functions.size
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_properties
|
119
|
+
input = <<~EOF
|
120
|
+
public class Properties {
|
121
|
+
public fun getNumber ()I
|
122
|
+
public final fun getId ()Ljava/lang/String;
|
123
|
+
public final fun getName ()Ljava/lang/String;
|
124
|
+
public final fun setName (Ljava/lang/String;)V
|
125
|
+
public final fun getFQDN ()Ljava/lang/String;
|
126
|
+
}
|
127
|
+
EOF
|
128
|
+
|
129
|
+
api = parser.parse(input)
|
130
|
+
assert_equal 0, api.classes.first.functions.size
|
131
|
+
properties = api.classes.first.properties
|
132
|
+
assert_equal 4, properties.size
|
133
|
+
|
134
|
+
number = properties[0]
|
135
|
+
assert_equal "number", number.name
|
136
|
+
assert_equal "Int", number.type
|
137
|
+
assert !number.is_writable?
|
138
|
+
|
139
|
+
id = properties[1]
|
140
|
+
assert_equal "id", id.name
|
141
|
+
assert_equal "String", id.type
|
142
|
+
assert !id.is_writable?
|
143
|
+
|
144
|
+
name = properties[2]
|
145
|
+
assert_equal "name", name.name
|
146
|
+
assert_equal "String", name.type
|
147
|
+
assert name.is_writable?
|
148
|
+
|
149
|
+
fqdn = properties[3]
|
150
|
+
assert_equal "fqdn", fqdn.name
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_companion_objects
|
154
|
+
# TypeCode
|
155
|
+
# Metadata
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_enums
|
159
|
+
input = <<~EOF
|
160
|
+
public final class com/package/Reason : java/lang/Enum {
|
161
|
+
public static final field GOOD Lcom/package/Reason;
|
162
|
+
public static final field NOT_SO_GOOD Lcom/package/Reason;
|
163
|
+
public static final field BAD Lcom/package/Reason;
|
164
|
+
public static final field NONE Lcom/package/Reason;
|
165
|
+
public static final field BFG1000_THING Lcom/package/Reason;
|
166
|
+
|
167
|
+
public final fun getCode ()I;
|
168
|
+
public final fun getName ()Ljava/lang/String;
|
169
|
+
public static fun valueOf (Ljava/lang/String;)Lcom/package/Reason;
|
170
|
+
public static fun values ()[LLcom/package/Reason;
|
171
|
+
}
|
172
|
+
EOF
|
173
|
+
|
174
|
+
api = parser.parse(input)
|
175
|
+
enums = api.enums
|
176
|
+
assert_equal 1, enums.size
|
177
|
+
|
178
|
+
reason = enums[0]
|
179
|
+
assert_equal "Reason", reason.name
|
180
|
+
assert_equal ["GOOD", "NOT_SO_GOOD", "BAD", "NONE", "BFG1000_THING"], reason.cases
|
181
|
+
assert_equal 2, reason.functions.size
|
182
|
+
assert_equal "static fun valueOf (String) -> Reason", reason.functions[0].full_signature
|
183
|
+
assert_equal "static fun values () -> [Reason]", reason.functions[1].full_signature
|
184
|
+
assert_equal 2, reason.properties.size
|
185
|
+
assert_equal "val code: Int", reason.properties[0].to_s
|
186
|
+
assert_equal "val name: String", reason.properties[1].to_s
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_normalization
|
190
|
+
input = <<~EOF
|
191
|
+
public class FirstClass {
|
192
|
+
public fun <init> ()V
|
193
|
+
public final fun finalAction ()V
|
194
|
+
public abstract fun abstractAction ()V
|
195
|
+
}
|
196
|
+
|
197
|
+
public final class com/package/Reason : java/lang/Enum {
|
198
|
+
public static final field GOOD Lcom/package/Reason;
|
199
|
+
public static final field NOT_SO_GOOD Lcom/package/Reason;
|
200
|
+
public static final field REALLY__UNCONVENTIONAL Lcom/package/Reason;
|
201
|
+
}
|
202
|
+
EOF
|
203
|
+
|
204
|
+
api = parser(normalize: true).parse(input)
|
205
|
+
|
206
|
+
first_class = api.classes.first
|
207
|
+
assert_equal 3, first_class.functions.size
|
208
|
+
assert_equal "init()", first_class.functions[0].full_signature
|
209
|
+
assert_equal "func finalAction() -> Void", first_class.functions[1].full_signature
|
210
|
+
assert_equal "func abstractAction() -> Void", first_class.functions[2].full_signature
|
211
|
+
|
212
|
+
reason = api.enums.first
|
213
|
+
assert_equal ["good", "notSoGood", "really_Unconventional"], reason.cases
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SwiftInterfaceParserTest < Minitest::Test
|
4
|
+
def parser
|
5
|
+
ApiDiff::SwiftInterfaceParser.new "strip-packages": true
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_returns_api
|
9
|
+
assert_instance_of ApiDiff::Api, parser.parse("")
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_classes
|
13
|
+
input = <<~EOF
|
14
|
+
public class First {
|
15
|
+
}
|
16
|
+
@_hasMissingDesignatedInitializers public class Second {
|
17
|
+
}
|
18
|
+
public class Third : Package.Parent {
|
19
|
+
}
|
20
|
+
public class Fourth : Swift.Codable, Swift.Hashable {
|
21
|
+
}
|
22
|
+
EOF
|
23
|
+
api = parser.parse(input)
|
24
|
+
classes = api.classes
|
25
|
+
assert_equal 4, classes.size
|
26
|
+
|
27
|
+
first = classes[0]
|
28
|
+
assert_equal "First", first.name
|
29
|
+
assert_equal "class First", first.declaration
|
30
|
+
|
31
|
+
second = classes[1]
|
32
|
+
assert_equal "Second", second.name
|
33
|
+
assert_equal "class Second", second.declaration
|
34
|
+
|
35
|
+
third = classes[2]
|
36
|
+
assert_equal "Third", third.name
|
37
|
+
assert_equal "class Third : Parent", third.declaration
|
38
|
+
|
39
|
+
fourth = classes[3]
|
40
|
+
assert_equal "Fourth", fourth.name
|
41
|
+
assert_equal "class Fourth : Codable, Hashable", fourth.declaration
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_properties
|
45
|
+
input = <<~EOF
|
46
|
+
public class FirstClass {
|
47
|
+
public var name: Swift.String?
|
48
|
+
public let number: Swift.Int
|
49
|
+
public var data: Foundation.Data {
|
50
|
+
get
|
51
|
+
}
|
52
|
+
public var detailed: [Swift.String] {
|
53
|
+
get
|
54
|
+
set
|
55
|
+
}
|
56
|
+
}
|
57
|
+
EOF
|
58
|
+
api = parser.parse(input)
|
59
|
+
properties = api.classes.first.properties
|
60
|
+
assert_equal 4, properties.size
|
61
|
+
|
62
|
+
name = properties[0]
|
63
|
+
assert_equal "name", name.name
|
64
|
+
assert_equal "String?", name.type
|
65
|
+
assert name.is_writable?
|
66
|
+
|
67
|
+
number = properties[1]
|
68
|
+
assert_equal "number", number.name
|
69
|
+
assert_equal "Int", number.type
|
70
|
+
assert !number.is_writable?
|
71
|
+
|
72
|
+
data = properties[2]
|
73
|
+
assert_equal "data", data.name
|
74
|
+
assert_equal "Data", data.type
|
75
|
+
assert !data.is_writable?
|
76
|
+
|
77
|
+
detailed = properties[3]
|
78
|
+
assert_equal "detailed", detailed.name
|
79
|
+
assert_equal "[String]", detailed.type
|
80
|
+
assert detailed.is_writable?
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_functions
|
84
|
+
input = <<~EOF
|
85
|
+
public class FirstClass {
|
86
|
+
public func reset() -> PromiseKit.Promise<Swift.Void>
|
87
|
+
public func hash(into hasher: inout Swift.Hasher)
|
88
|
+
@available(iOS 13, *)
|
89
|
+
public func encode(to encoder: Swift.Encoder) throws
|
90
|
+
public init(identifier: Swift.String? = nil, name: Swift.String? = nil)
|
91
|
+
public static func == (lhs: Package.FirstClass, rhs: Package.FirstClass) -> Swift.Bool
|
92
|
+
public func collect(from source: Package.Source, progress progressHandler: ((Swift.Double) -> Swift.Void)?, completion completionHandler: @escaping (Swift.Error?) -> Swift.Void) -> Swift.Int
|
93
|
+
}
|
94
|
+
EOF
|
95
|
+
|
96
|
+
api = parser.parse(input)
|
97
|
+
functions = api.classes.first.functions
|
98
|
+
assert_equal 6, functions.size
|
99
|
+
|
100
|
+
reset = functions[0]
|
101
|
+
assert_equal "reset", reset.name
|
102
|
+
assert_equal "func reset() -> Promise<Void>", reset.full_signature
|
103
|
+
|
104
|
+
hash = functions[1]
|
105
|
+
assert_equal "hash", hash.name
|
106
|
+
assert_equal "func hash(into: inout Hasher)", hash.full_signature
|
107
|
+
|
108
|
+
encode = functions[2]
|
109
|
+
assert_equal "encode", encode.name
|
110
|
+
assert_equal "func encode(to: Encoder) throws", encode.full_signature
|
111
|
+
|
112
|
+
init = functions[3]
|
113
|
+
assert_equal "init", init.name
|
114
|
+
assert_equal "init(identifier: String? = nil, name: String? = nil)", init.full_signature
|
115
|
+
|
116
|
+
equals = functions[4]
|
117
|
+
assert_equal "==", equals.name
|
118
|
+
assert_equal "static func == (lhs: FirstClass, rhs: FirstClass) -> Bool", equals.full_signature
|
119
|
+
|
120
|
+
collect = functions[5]
|
121
|
+
assert_equal "collect", collect.name
|
122
|
+
assert_equal "func collect(from: Source, progress: ((Double) -> Void)?, completion: @escaping (Error?) -> Void) -> Int", collect.full_signature
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_class_extensions
|
126
|
+
input = <<~EOF
|
127
|
+
public class ExtFunction {
|
128
|
+
}
|
129
|
+
extension ExtFunction {
|
130
|
+
public static func == (lhs: ExtFunction, rhs: ExtFunction) -> Swift.Bool
|
131
|
+
@available(iOS 13, *)
|
132
|
+
public func hash(into hasher: inout Swift.Hasher)
|
133
|
+
}
|
134
|
+
public class ExtProperty {
|
135
|
+
}
|
136
|
+
extension ExtProperty {
|
137
|
+
public var number: Swift.Int {
|
138
|
+
get
|
139
|
+
}
|
140
|
+
}
|
141
|
+
public class ExtParent {
|
142
|
+
}
|
143
|
+
extension ExtParent : Swift.Hashable {
|
144
|
+
}
|
145
|
+
EOF
|
146
|
+
|
147
|
+
api = parser.parse(input)
|
148
|
+
classes = api.classes
|
149
|
+
assert_equal 3, classes.size
|
150
|
+
|
151
|
+
ext_function = classes[0]
|
152
|
+
assert_equal 2, ext_function.functions.size
|
153
|
+
assert_equal "==", ext_function.functions[0].name
|
154
|
+
assert_equal "hash", ext_function.functions[1].name
|
155
|
+
|
156
|
+
ext_property = classes[1]
|
157
|
+
assert_equal 1, ext_property.properties.size
|
158
|
+
assert_equal "number", ext_property.properties[0].name
|
159
|
+
assert !ext_property.properties[0].is_writable?
|
160
|
+
|
161
|
+
ext_parent = classes[2]
|
162
|
+
assert_equal 1, ext_parent.parents.size
|
163
|
+
assert_equal "Hashable", ext_parent.parents[0]
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_interfaces
|
167
|
+
input = <<~EOF
|
168
|
+
public protocol WithFunctions {
|
169
|
+
func action(name: Swift.String)
|
170
|
+
@available(iOS 13, *)
|
171
|
+
func query(_ query: Query) -> PromiseKit.Promise<[Document]>
|
172
|
+
}
|
173
|
+
public protocol WithProperties {
|
174
|
+
static var prop: [Self] { get }
|
175
|
+
}
|
176
|
+
EOF
|
177
|
+
|
178
|
+
api = parser.parse(input)
|
179
|
+
interfaces = api.interfaces
|
180
|
+
assert_equal 2, interfaces.size
|
181
|
+
|
182
|
+
with_functions = interfaces[0]
|
183
|
+
assert_equal 2, with_functions.functions.size
|
184
|
+
assert_equal "action", with_functions.functions[0].name
|
185
|
+
assert_equal "query", with_functions.functions[1].name
|
186
|
+
|
187
|
+
with_properties = interfaces[1]
|
188
|
+
assert_equal 1, with_properties.properties.size
|
189
|
+
prop = with_properties.properties[0]
|
190
|
+
assert_equal "prop", prop.name
|
191
|
+
assert_equal "[Self]", prop.type
|
192
|
+
assert prop.is_static?
|
193
|
+
assert !prop.is_writable?
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_interface_extensions
|
197
|
+
input = <<~EOF
|
198
|
+
public protocol WithFunctions {
|
199
|
+
}
|
200
|
+
extension WithFunctions : Swift.Hashable {
|
201
|
+
public static func == (lhs: WithFunctions, rhs: WithFunctions) -> Swift.Bool
|
202
|
+
public func hash(into hasher: inout Swift.Hasher)
|
203
|
+
}
|
204
|
+
public protocol WithProperties {
|
205
|
+
}
|
206
|
+
extension WithProperties {
|
207
|
+
public var hashValue: Swift.Int {
|
208
|
+
get
|
209
|
+
}
|
210
|
+
}
|
211
|
+
public protocol Delegate : AnyObject {
|
212
|
+
func deactivate()
|
213
|
+
}
|
214
|
+
extension Delegate {
|
215
|
+
public func deactivate()
|
216
|
+
}
|
217
|
+
EOF
|
218
|
+
|
219
|
+
api = parser.parse(input)
|
220
|
+
interfaces = api.interfaces
|
221
|
+
assert_equal 3, interfaces.size
|
222
|
+
|
223
|
+
with_functions = interfaces[0]
|
224
|
+
assert_equal ["Hashable"], with_functions.parents
|
225
|
+
assert_equal 2, with_functions.functions.size
|
226
|
+
assert_equal "==", with_functions.functions[0].name
|
227
|
+
assert_equal "hash", with_functions.functions[1].name
|
228
|
+
|
229
|
+
with_properties = interfaces[1]
|
230
|
+
assert_equal 1, with_properties.properties.size
|
231
|
+
assert_equal "hashValue", with_properties.properties[0].name
|
232
|
+
|
233
|
+
delegate = interfaces[2]
|
234
|
+
assert_equal 1, delegate.functions.size
|
235
|
+
assert_equal "deactivate", delegate.functions[0].name
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_enums
|
239
|
+
input = <<~EOF
|
240
|
+
public enum Alpha {
|
241
|
+
case a
|
242
|
+
}
|
243
|
+
@frozen public enum Beta {
|
244
|
+
case c
|
245
|
+
case d
|
246
|
+
}
|
247
|
+
@frozen public enum Gamma : Swift.String, Swift.CaseIterable {
|
248
|
+
case e
|
249
|
+
case f
|
250
|
+
case g
|
251
|
+
}
|
252
|
+
EOF
|
253
|
+
|
254
|
+
api = parser.parse(input)
|
255
|
+
enums = api.enums
|
256
|
+
assert_equal 3, enums.size
|
257
|
+
|
258
|
+
alpha = enums[0]
|
259
|
+
assert_equal "Alpha", alpha.name
|
260
|
+
assert_equal ["a"], alpha.cases
|
261
|
+
|
262
|
+
beta = enums[1]
|
263
|
+
assert_equal "Beta", beta.name
|
264
|
+
assert_equal ["c", "d"], beta.cases
|
265
|
+
|
266
|
+
gamma = enums[2]
|
267
|
+
assert_equal "Gamma", gamma.name
|
268
|
+
assert_equal 3, gamma.cases.size
|
269
|
+
assert_equal ["e", "f", "g"], gamma.cases
|
270
|
+
assert_equal ["String", "CaseIterable"], gamma.parents
|
271
|
+
end
|
272
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_diff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Ludwig
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '11'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '11'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- sebastian@lurado.de
|
44
|
+
executables:
|
45
|
+
- api_diff
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- Gemfile
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- exe/api_diff
|
53
|
+
- lib/api_diff.rb
|
54
|
+
- lib/api_diff/api.rb
|
55
|
+
- lib/api_diff/class.rb
|
56
|
+
- lib/api_diff/cli.rb
|
57
|
+
- lib/api_diff/enum.rb
|
58
|
+
- lib/api_diff/function.rb
|
59
|
+
- lib/api_diff/interface.rb
|
60
|
+
- lib/api_diff/kotlin_bcv_parser.rb
|
61
|
+
- lib/api_diff/parser.rb
|
62
|
+
- lib/api_diff/property.rb
|
63
|
+
- lib/api_diff/swift_interface_parser.rb
|
64
|
+
- lib/api_diff/type.rb
|
65
|
+
- lib/api_diff/version.rb
|
66
|
+
- test/kotlin_bcv_parser_test.rb
|
67
|
+
- test/swift_interface_parser_test.rb
|
68
|
+
- test/test_helper.rb
|
69
|
+
homepage: https://github.com/sebastianludwig/api-diff
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata:
|
73
|
+
homepage_uri: https://github.com/sebastianludwig/api-diff
|
74
|
+
source_code_uri: https://github.com/sebastianludwig/api_diff
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 2.3.0
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubygems_version: 3.1.4
|
91
|
+
signing_key:
|
92
|
+
specification_version: 4
|
93
|
+
summary: Bring APIs into an easily diff-able format
|
94
|
+
test_files:
|
95
|
+
- test/swift_interface_parser_test.rb
|
96
|
+
- test/test_helper.rb
|
97
|
+
- test/kotlin_bcv_parser_test.rb
|