api_diff 0.2.0 → 0.3.0

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