Commit 007e21c3 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Improve compare script to produce more extensive diff.

The compare script now lists added, removed and changed members.

The change tracking is currently targeted towards configs. We
detect the following changes:  type, default value, visibility.

I refactored the script so it now finally has a sensible interface,
with options --ignore (for the optional ignored methods/classes file)
and --type (to limit output to just configs).
parent 85d03eff
Loading
Loading
Loading
Loading
+189 −35
Original line number Diff line number Diff line
#!/usr/bin/env ruby
# Compare .json files of different ExtJS versions
# Produces diff of two JSDuck exports.

# For running when gem not installed
$:.unshift File.dirname(File.dirname(__FILE__)) + "/lib"

require "rubygems"
require "cgi"
require "optparse"
require "jsduck/json_duck"

options = {}
input_files = OptionParser.new do |opts|
  opts.banner = "Produces diff of two JSDuck exports.\n\n" +
    "Usage: compare [options] old/classes/ new/classes/ output.html\n\n"

  opts.on("--ignore=FILE", "A file listing members to be ignored") do |file|
    options[:ignore] = file
  end

  opts.on("--type=NAME", "Only produce diff of cfg, property, method or event.") do |name|
    options[:type] = name
  end

  opts.on("-h", "--help", "Show this help message") do
    puts opts
    exit
  end
end.parse!

if input_files.length == 3
  options[:old_classes] = input_files[0]
  options[:new_classes] = input_files[1]
  options[:out_file] = input_files[2]
else
  puts "You must specify exactly 3 filenames: old/classes/ new/classes/ output.html"
  exit 1
end


def read_class(filename)
  JsDuck::JsonDuck.read(filename)
end

# loops through all members of class
def each_member(cls)
  ["members", "statics"].each do |category|
    cls[category].each_pair do |k, members|
      members.each do |m|
        yield m
      end
    end
  end
end

def discard_docs(cls)
  each_member(cls) do |m|
    m["doc"] = nil
    m["params"] = nil
  end
  cls
end

def read_all_classes(dir, ignore_private=false)
  map = {}
  Dir[dir+"/*.json"].each do |filename|
    print "."
    STDOUT.flush
    cls = read_class(filename)
    cls = discard_docs(read_class(filename))
    unless ignore_private && cls["private"]
      map[cls["name"]] = cls
      cls["alternateClassNames"].each do |name|
@@ -40,24 +90,86 @@ def normal_public_member?(m)
  !m["private"] && !m["meta"]["protected"] && !m["meta"]["deprecated"] && !m["meta"]["removed"]
end

# Gathers class members that are in cls1, but are missing in cls2
# Ignoring members listed in ignored_members hash.
def compare_classes(cls1, cls2, ignored_members)
def unify_default(v)
  (v || "undefined").gsub(/"/, "'").sub(/^Ext\.baseCSSPrefix \+ '/, "'x-")
end

def visibility(m)
  if m["private"]
    "private"
  elsif m["meta"]["protected"]
    "protected"
  else
    "public"
  end
end

# Finds equivalent member to given member from class.
# Returns the member found or nil.
def find_member(cls, member)
  cls[member["meta"]["static"] ? "statics" : "members"][member["tagname"]].find do |m|
    member["name"] == m["name"]
  end
end


# Compares members of cls1 to cls2.
# Returns diff array of added, removed and changed members.
def compare_classes(cls1, cls2)
  diff = []
  cls1["members"].each_pair do |group_name, group_items|
    group_items.find_all {|m1| normal_public_member?(m1) && m1["owner"] == cls1["name"] }.each do |m1|
      match = cls2["members"][group_name].find do |m2|
        m2["name"] == m1["name"] && !m2["meta"]["protected"] && !m2["private"]

  checked_members = {}

  each_member(cls1) do |m1|
    next unless normal_public_member?(m1) && m1["owner"] == cls1["name"]

    checked_members[m1["id"]] = true

    m2 = find_member(cls2, m1)

    if m2
      changes = []
      if m1["type"] != m2["type"]
        changes << {
          :what => "type",
          :a => m1["type"],
          :b => m2["type"],
        }
      end
      if unify_default(m1["default"]) != unify_default(m2["default"])
        changes << {
          :what => "default",
          :a => unify_default(m1["default"]),
          :b => unify_default(m2["default"]),
        }
      end
      if visibility(m1) != visibility(m2)
        changes << {
          :what => "visibility",
          :a => visibility(m1),
          :b => visibility(m2),
        }
      end

      if changes.length > 0
        diff << {
          :type => m1["tagname"],
          :name => m1["name"],
          :what => "changed",
          :changes => changes,
        }
      end
      if !match && m1["name"] != "constructor" && m1["name"] != "" && !ignored_members[cls1["name"]+"#"+m1["name"]]

    elsif !m2 && m1["name"] != "constructor" && m1["name"] != ""
      other = nil
      ["cfg", "property", "method", "event"].each do |g|
        other = other || cls2["members"][g].find {|m2| m2["name"] == m1["name"] }
        other = other || cls2["statics"][g].find {|m2| m2["name"] == m1["name"] }
      end
      diff << {
          :type => group_name,
        :type => m1["tagname"],
        :name => m1["name"],
        :what => "removed",
        :other => other ? {
          :type => other["tagname"],
          :static => other["meta"]["static"],
@@ -67,15 +179,47 @@ def compare_classes(cls1, cls2, ignored_members)
      }
    end
  end

  each_member(cls2) do |m2|
    next unless normal_public_member?(m2) && m2["owner"] == cls2["name"] && !checked_members[m2["id"]]

    m1 = find_member(cls1, m2)

    if m1
      changes = []
      if visibility(m1) != visibility(m2)
        changes << {
          :what => "visibility",
          :a => visibility(m1),
          :b => visibility(m2),
        }
      end

      if changes.length > 0
        diff << {
          :type => m2["tagname"],
          :name => m2["name"],
          :what => "changed",
          :changes => changes,
        }
      end

    elsif !m1 && m2["name"] != "constructor" && m2["name"] != ""
      diff << {
        :type => m2["tagname"],
        :name => m2["name"],
        :what => "added",
      }
    end
  end

  diff
end


old_classes = read_all_classes(ARGV[0], :ignore_private)
new_classes = read_all_classes(ARGV[1])
out_file = ARGV[2]
ignored_members = ARGV[3] ? read_ignored_members(ARGV[3]) : {}
old_classes = read_all_classes(options[:old_classes], :ignore_private)
new_classes = read_all_classes(options[:new_classes])
ignored_members = options[:ignore] ? read_ignored_members(options[:ignore]) : {}

# Explicit remapping of classes
remap = {
@@ -101,16 +245,23 @@ old_classes.each_pair do |name, cls|
      :name => cls["name"],
      :found => !!new_cls || ignored_members[name],
      :new_name => new_cls && new_cls["name"],
      :diff => new_cls ? compare_classes(cls, new_cls, ignored_members) : [],
      :diff => new_cls ? compare_classes(cls, new_cls) : [],
    }
  end
end

# Throw away ignored members and everything except configs
diff_data.each do |cls|
  cls[:diff] = cls[:diff].find_all do |m|
    !ignored_members[cls[:name]+"#"+m[:name]] && (!options[:type] || m[:type] == options[:type])
  end
end

# Choose title based on filename
if out_file =~ /touch/
if options[:out_file] =~ /touch/
  title = "Comparison of Touch 1.1.1 and Touch 2.0.0"
else
  title = "Comparison of Ext 4.0.7 and Ext 4.1.0"
  title = "Comparison of Ext 4.0.7 and Ext 4.1.1"
end

# do HTML output
@@ -146,20 +297,24 @@ diff_data.each do |cls|
    html << "<li>"
    if cls[:found]
      new_name = (cls[:new_name] == cls[:name] ? "" : " --> " + cls[:new_name])
      diff_count = cls[:diff].length > 0 ? "(#{cls[:diff].length} missing members)" : ""
      diff_count = cls[:diff].length > 0 ? "(#{cls[:diff].length} changes)" : ""
      link = cls[:diff].length > 0 ? "<a href='#expand'>#{cls[:name]}</a>" : cls[:name]
      html << "<h2>#{link} #{new_name} #{diff_count}</h2>"
      if cls[:diff].length > 0
        html << "<ul>"
        cls[:diff].each do |m|
          html << "<li>"
          html << m[:type] + " " + m[:name]
          html << m[:what] + " " + m[:type] + " " + m[:name]
          if m[:other]
            o = m[:other]
            stat = o[:static] ? 'static' : ''
            priv = o[:private] ? 'private' : ''
            prot = o[:protected] ? 'protected' : ''
            html << " (found #{stat} #{priv} #{prot} #{o[:type]} with the same name)"
          elsif m[:changes]
            m[:changes].each do |c|
              html << " (#{c[:what]} changed from #{CGI.escapeHTML(c[:a])} to #{CGI.escapeHTML(c[:b])})"
            end
          end
          html << "</li>"
        end
@@ -178,12 +333,11 @@ dd = diff_data
html << "<p>" + dd.find_all {|c| !c[:found] }.length.to_s + " classes not found</p>"
html << "<p>" + dd.find_all {|c| c[:found] && c[:name] == c[:new_name] }.length.to_s + " classes with same name</p>"
html << "<p>" + dd.find_all {|c| c[:found] && c[:name] != c[:new_name] }.length.to_s + " renamed classes</p>"
html << "<p>" + dd.find_all {|c| c[:diff].length > 0 }.length.to_s + " classes with missing members</p>"
html << "<p>" + dd.map {|c| c[:diff].length }.inject {|sum,x| sum + x }.to_s + " missing members</p>"
html << "<p>" + dd.map {|c| c[:diff].find_all {|m| !m[:other]}.length }.inject {|sum,x| sum + x }.to_s + " completely missing members</p>"
html << "<p>" + dd.find_all {|c| c[:diff].length > 0 }.length.to_s + " classes with changed members</p>"
html << "<p>" + dd.map {|c| c[:diff].length }.inject {|sum,x| sum + x }.to_s + " changed members</p>"


html << "</body>"
html << "</html>"

File.open(out_file, 'w') {|f| f.write(html.join("\n")) }
File.open(options[:out_file], 'w') {|f| f.write(html.join("\n")) }