class ApiDiff::KotlinBCVParser

Biggest Drawback: Does not support optionals :-/

Public Instance Methods

parse(content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 4
def parse(content)
  Property.readonly_keyword = "val" unless @options[:normalize]

  sections = content.scan(/^.+?{$.*?^}$/m)
  sections.each do |section|
    section.strip!
    first_line = section.split("\n")[0]
    if first_line.include?(" : java/lang/Enum")
      parse_enum(section)
    elsif first_line.include?("interface class")
      parse_interface(section)
    elsif first_line.match?(/public.+class/)
      parse_class(section)
    end
  end

  normalize!(api) if @options[:normalize]
end

Private Instance Methods

extract_properties(type) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 87
def extract_properties(type)
  getters = type.functions.select { |f| f.signature.match(/fun get[A-Z](\w+)? \(\)/) }
  getters.each do |getter|
    setter_name = getter.name.gsub(/^get/, "set")
    setter = type.functions.find { |f| f.signature.match(/fun #{setter_name} \(#{getter.return_type}\)/) }

    type.functions.delete getter
    type.functions.delete setter if setter

    name = getter.name.gsub(/^get/, "")
    if name == name.upcase  # complete uppercase -> complete lowercase
      name.downcase!
    else
      name[0] = name[0].downcase
    end
    type.properties << Property.new(
      name: name,
      type: getter.return_type,
      writable: (setter != nil),
      static: getter.is_static?
    )
  end
end
map_jvm_types(types) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 115
def map_jvm_types(types)
  mapping = {
    "Z" => "Boolean",
    "B" => "Byte",
    "C" => "Char",
    "S" => "Short",
    "I" => "Int",
    "J" => "Long",
    "F" => "Float",
    "D" => "Double",
    "V" => "Void"
  }
  vm_types_regexp = /(?<array>\[)?(?<type>Z|B|C|S|I|J|F|D|V|(L(?<class>[^;]+);))/
  all_matches(types, vm_types_regexp).map do |match|
    if match[:class]
      result = unqualify(transform_package_path(match[:class]))
    else
      result = mapping[match[:type]]
    end
    result = "[#{result}]" if match[:array]
    result
  end
end
normalize!(api) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 139
def normalize!(api)
  api.classes.reject! { |c| c.parents == ["Factory"] }
  api.classes.reject! { |c| c.name.start_with? "Dagger" }

  # remove abstract & final
  # fun -> func
  # <init> -> init
  # remove space before (
  (api.classes + api.interfaces + api.enums).flat_map(&:functions).each do |f|
    f.signature.gsub!(/(?:abstract )?(?:final )?fun (<?\w+>?) \(/, "func \\1(")
    f.signature.gsub!("func <init>", "init")
  end

  # enum screaming case -> camel case
  api.enums.each do |e|
    e.cases = e.cases.map do |c|
      # supports double _ by preserving one of them
      c.scan(/_?[A-Z0-9]+_?/).map.with_index do |p, index|
        p.gsub(/_$/, "").downcase.gsub(/^_?\w/) { |m| index == 0 ? m : m.upcase }
      end.join
    end
  end

end
parse_class(class_content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 25
def parse_class(class_content)
  qualified_name = transform_package_path class_content.match(/public.+class ([^\s]+)/)[1]

  cls = Class.new(unqualify(qualified_name), qualified_name)
  cls.parents = parse_parents(class_content)
  cls.functions = parse_functions(class_content)
  extract_properties(cls)
  api.classes << cls
end
parse_enum(enum_content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 46
def parse_enum(enum_content)
  qualified_name = transform_package_path enum_content.match(/public.+class ([^\s]+)/)[1]

  enum = Enum.new(unqualify(qualified_name), qualified_name)
  enum.cases = parse_enum_cases(enum_content)
  enum.functions = parse_functions(enum_content)
  extract_properties(enum)
  api.enums << enum
end
parse_enum_cases(content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 80
def parse_enum_cases(content)
  case_regexp = /public static final field (?<name>[A-Z_0-9]+)/
  all_matches(content, case_regexp).map do |match|
    match[:name]
  end
end
parse_functions(content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 62
def parse_functions(content)
  method_regexp = /public (?<signature>(?<static>static )?.*fun (?:(<(?<init>init)>)|(?<name>[^\s]+)) \((?<params>.*)\))(?<return_type>.+)$/
  all_matches(content, method_regexp).map do |match|
    next if match[:name]&.start_with? "component" # don't add data class `componentX` methods
    params_range = ((match.begin(:params) - match.begin(:signature))...(match.end(:params) - match.begin(:signature)))
    signature = match[:signature]
    signature[params_range] = map_jvm_types(match[:params]).join(", ")
    signature.gsub!(/synthetic ?/, "") # synthetic or not, it's part of the API
    Function.new(
      name: (match[:name] || match[:init]),
      signature: signature,
      return_type: match[:init].nil? ? map_jvm_types(match[:return_type]).join : nil,
      static: !match[:static].nil?,
      constructor: (not match[:init].nil?)
    )
  end.compact
end
parse_interface(interface_content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 35
def parse_interface(interface_content)
  qualified_name = transform_package_path interface_content.match(/public.+class ([^\s]+)/)[1]

  interface = Interface.new(unqualify(qualified_name), qualified_name)
  interface.parents = parse_parents(interface_content)
  interface.functions = parse_functions(interface_content)
  extract_properties(interface)

  api.interfaces << interface
end
parse_parents(content) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 56
def parse_parents(content)
  parents_match = content.match(/\A.+?: (.+?) \{$/)
  return [] if parents_match.nil?
  parents_match[1].split(",").map { |p| unqualify(transform_package_path(p.strip)) }
end
transform_package_path(path) click to toggle source
# File lib/api_diff/kotlin_bcv_parser.rb, line 111
def transform_package_path(path)
  path.gsub("/", ".")
end