api_diff 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in api_diff.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "api_diff"
4
+
5
+ begin
6
+ ApiDiff::Cli.new.run!(ARGV)
7
+ rescue ApiDiff::Error => e
8
+ abort e.message
9
+ end
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
@@ -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
@@ -0,0 +1,4 @@
1
+ module ApiDiff
2
+ class Class < Type
3
+ end
4
+ end
@@ -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,12 @@
1
+ module ApiDiff
2
+ class Enum < Type
3
+ attr_accessor :cases
4
+
5
+ def sections
6
+ [
7
+ cases.sort.map { |c| "case #{c}" },
8
+ *super
9
+ ]
10
+ end
11
+ end
12
+ 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,4 @@
1
+ module ApiDiff
2
+ class Interface < Type
3
+ end
4
+ 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,3 @@
1
+ module ApiDiff
2
+ VERSION = "0.2.0"
3
+ 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
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
2
+ require "api_diff"
3
+
4
+ require "minitest/autorun"
5
+ require "byebug"
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