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 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