api_diff 0.2.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 +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
|