api_diff 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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