| 
 | 1 | +#!/usr/bin/env ruby  | 
 | 2 | + | 
 | 3 | +# frozen_string_literal: true  | 
 | 4 | + | 
 | 5 | +require "json"  | 
 | 6 | + | 
 | 7 | +head_stats = JSON.parse(File.read(ENV["CURRENT_STATS_PATH"]), symbolize_names: true)  | 
 | 8 | +base_stats = JSON.parse(File.read(ENV["BASE_STATS_PATH"]), symbolize_names: true)  | 
 | 9 | + | 
 | 10 | +def format_for_code_block(data)  | 
 | 11 | +  data.map do |item|  | 
 | 12 | +    formatted_string = +"#{item[:path]}:#{item[:line]}"  | 
 | 13 | +    formatted_string << "\n└── #{item[:line_content]}" if item[:line_content]  | 
 | 14 | +    formatted_string  | 
 | 15 | +  end.join("\n")  | 
 | 16 | +end  | 
 | 17 | + | 
 | 18 | +def pluralize(word, suffix = "s")  | 
 | 19 | +  "#{word}#{suffix}"  | 
 | 20 | +end  | 
 | 21 | + | 
 | 22 | +def concord(word, count, suffix = "s")  | 
 | 23 | +  (count > 1) ? pluralize(word, suffix) : word  | 
 | 24 | +end  | 
 | 25 | + | 
 | 26 | +def create_intro(  | 
 | 27 | +  added:,  | 
 | 28 | +  removed:,  | 
 | 29 | +  data_name:,  | 
 | 30 | +  added_partially: [],  | 
 | 31 | +  removed_partially: [],  | 
 | 32 | +  data_name_partially: nil,  | 
 | 33 | +  base_percentage: nil,  | 
 | 34 | +  head_percentage: nil,  | 
 | 35 | +  percentage_data_name: nil  | 
 | 36 | +)  | 
 | 37 | +  intro = +"This PR "  | 
 | 38 | +  intro << "introduces " if added.any? || added_partially.any?  | 
 | 39 | +  intro << "**#{added.size}** #{concord(data_name, added.size)}" if added.any?  | 
 | 40 | +  intro << " and " if added.any? && added_partially.any?  | 
 | 41 | +  intro << "**#{added_partially.size}** #{concord(data_name_partially, added_partially.size)}" if added_partially.any?  | 
 | 42 | +  intro << ", and " if (added.any? || added_partially.any?) && (removed.any? || removed_partially.any?)  | 
 | 43 | +  intro << "clears " if removed.any? || removed_partially.any?  | 
 | 44 | +  intro << "**#{removed.size}** #{concord(data_name, removed.size)}" if removed.any?  | 
 | 45 | +  intro << " and " if removed.any? && removed_partially.any?  | 
 | 46 | +  intro << "**#{removed_partially.size}** #{concord(data_name_partially, removed_partially.size)}" if removed_partially.any?  | 
 | 47 | +  if base_percentage != head_percentage  | 
 | 48 | +    intro << ". It #{(base_percentage > head_percentage) ? "decreases" : "increases"} "  | 
 | 49 | +    intro << "the percentage of #{pluralize(percentage_data_name)} from #{base_percentage}% to #{head_percentage}% "  | 
 | 50 | +    intro << "(**#{"+" if head_percentage > base_percentage}#{(head_percentage - base_percentage).round(2)}**%)"  | 
 | 51 | +  end  | 
 | 52 | +  intro << "."  | 
 | 53 | +  intro  | 
 | 54 | +end  | 
 | 55 | + | 
 | 56 | +def create_summary(  | 
 | 57 | +  added:,  | 
 | 58 | +  removed:,  | 
 | 59 | +  data_name:,  | 
 | 60 | +  added_partially: [],  | 
 | 61 | +  removed_partially: [],  | 
 | 62 | +  data_name_partially: nil,  | 
 | 63 | +  base_percentage: nil,  | 
 | 64 | +  head_percentage: nil,  | 
 | 65 | +  percentage_data_name: nil  | 
 | 66 | +)  | 
 | 67 | +  return [nil, 0] if added.empty? && removed.empty? && added_partially.empty? && removed_partially.empty?  | 
 | 68 | + | 
 | 69 | +  intro = create_intro(  | 
 | 70 | +    added: added,  | 
 | 71 | +    removed: removed,  | 
 | 72 | +    data_name: data_name,  | 
 | 73 | +    added_partially: added_partially,  | 
 | 74 | +    removed_partially: removed_partially,  | 
 | 75 | +    data_name_partially: data_name_partially,  | 
 | 76 | +    base_percentage: base_percentage,  | 
 | 77 | +    head_percentage: head_percentage,  | 
 | 78 | +    percentage_data_name: percentage_data_name  | 
 | 79 | +  )  | 
 | 80 | + | 
 | 81 | +  summary = +"### #{pluralize(data_name).capitalize}\n"  | 
 | 82 | +  summary << "#{intro}\n"  | 
 | 83 | +  if added.any? || removed.any?  | 
 | 84 | +    summary << "<details><summary>#{pluralize(data_name).capitalize} (<strong>+#{added&.size || 0}-#{removed.size || 0}</strong>)</summary>\n"  | 
 | 85 | +    if added.any?  | 
 | 86 | +      summary << "  ❌ <em>Introduced:</em>\n"  | 
 | 87 | +      summary << "  <pre><code>#{format_for_code_block(added)}</code></pre>\n"  | 
 | 88 | +    end  | 
 | 89 | +    if removed.any?  | 
 | 90 | +      summary << "  ✅ <em>Cleared:</em>\n"  | 
 | 91 | +      summary << "  <pre><code>#{format_for_code_block(removed)}</code></pre>\n"  | 
 | 92 | +    end  | 
 | 93 | +    summary << "</details>\n"  | 
 | 94 | +  end  | 
 | 95 | +  if added_partially.any? || removed_partially.any?  | 
 | 96 | +    summary << "<details><summary>#{pluralize(data_name_partially).capitalize} (<strong>+#{added_partially.size || 0}-#{removed_partially.size || 0}</strong>)</summary>\n"  | 
 | 97 | +    if added_partially.any?  | 
 | 98 | +      summary << "  ❌ <em>Introduced:</em>\n"  | 
 | 99 | +      summary << "  <pre><code>#{format_for_code_block(added_partially)}</code></pre>\n"  | 
 | 100 | +    end  | 
 | 101 | +    if removed_partially.any?  | 
 | 102 | +      summary << "  ✅ <em>Cleared:</em>\n"  | 
 | 103 | +      summary << "  <pre><code>#{format_for_code_block(removed_partially)}</code></pre>\n"  | 
 | 104 | +    end  | 
 | 105 | +    summary << "</details>\n"  | 
 | 106 | +  end  | 
 | 107 | +  summary << "\n"  | 
 | 108 | +  total_introduced = (added&.size || 0) + (added_partially&.size || 0)  | 
 | 109 | +  [summary, total_introduced]  | 
 | 110 | +end  | 
 | 111 | + | 
 | 112 | +def ignored_files_summary(head_stats, base_stats)  | 
 | 113 | +  # This will skip the summary if files are added/removed from contrib folders for now.  | 
 | 114 | +  ignored_files_added = head_stats[:ignored_files] - base_stats[:ignored_files]  | 
 | 115 | +  ignored_files_removed = base_stats[:ignored_files] - head_stats[:ignored_files]  | 
 | 116 | + | 
 | 117 | +  return [nil, 0] if ignored_files_added.empty? && ignored_files_removed.empty?  | 
 | 118 | + | 
 | 119 | +  typed_files_percentage_base = ((base_stats[:total_files_size] - base_stats[:ignored_files].size) / base_stats[:total_files_size].to_f * 100).round(2)  | 
 | 120 | +  typed_files_percentage_head = ((head_stats[:total_files_size] - head_stats[:ignored_files].size) / head_stats[:total_files_size].to_f * 100).round(2)  | 
 | 121 | + | 
 | 122 | +  intro = create_intro(  | 
 | 123 | +    added: ignored_files_added,  | 
 | 124 | +    removed: ignored_files_removed,  | 
 | 125 | +    data_name: "ignored file",  | 
 | 126 | +    base_percentage: typed_files_percentage_base,  | 
 | 127 | +    head_percentage: typed_files_percentage_head,  | 
 | 128 | +    percentage_data_name: "typed file"  | 
 | 129 | +  )  | 
 | 130 | + | 
 | 131 | +  summary = +"### Ignored files\n"  | 
 | 132 | +  summary << "#{intro}\n"  | 
 | 133 | +  summary << "<details><summary>Ignored files (<strong>+#{ignored_files_added&.size || 0}-#{ignored_files_removed&.size || 0}</strong>)</summary>\n"  | 
 | 134 | +  if ignored_files_added.any?  | 
 | 135 | +    summary << "  ❌ <em>Introduced:</em>\n"  | 
 | 136 | +    summary << "  <pre><code>#{ignored_files_added.join("\n")}</code></pre>\n"  | 
 | 137 | +  end  | 
 | 138 | +  if ignored_files_removed.any?  | 
 | 139 | +    summary << "  ✅ <em>Cleared:</em>\n"  | 
 | 140 | +    summary << "  <pre><code>#{ignored_files_removed.join("\n")}</code></pre>\n"  | 
 | 141 | +  end  | 
 | 142 | +  summary << "</details>\n"  | 
 | 143 | +  summary << "\n"  | 
 | 144 | +  total_introduced = ignored_files_added&.size || 0  | 
 | 145 | +  [summary, total_introduced]  | 
 | 146 | +end  | 
 | 147 | + | 
 | 148 | +def steep_ignore_summary(head_stats, base_stats)  | 
 | 149 | +  steep_ignore_added = head_stats[:steep_ignore_comments] - base_stats[:steep_ignore_comments]  | 
 | 150 | +  steep_ignore_removed = base_stats[:steep_ignore_comments] - head_stats[:steep_ignore_comments]  | 
 | 151 | + | 
 | 152 | +  create_summary(  | 
 | 153 | +    added: steep_ignore_added,  | 
 | 154 | +    removed: steep_ignore_removed,  | 
 | 155 | +    data_name: "<code>steep:ignore</code> comment"  | 
 | 156 | +  )  | 
 | 157 | +end  | 
 | 158 | + | 
 | 159 | +def untyped_methods_summary(head_stats, base_stats)  | 
 | 160 | +  untyped_methods_added = head_stats[:untyped_methods] - base_stats[:untyped_methods]  | 
 | 161 | +  untyped_methods_removed = base_stats[:untyped_methods] - head_stats[:untyped_methods]  | 
 | 162 | +  partially_typed_methods_added = head_stats[:partially_typed_methods] - base_stats[:partially_typed_methods]  | 
 | 163 | +  partially_typed_methods_removed = base_stats[:partially_typed_methods] - head_stats[:partially_typed_methods]  | 
 | 164 | +  total_methods_base = base_stats[:typed_methods_size] + base_stats[:untyped_methods].size + base_stats[:partially_typed_methods].size  | 
 | 165 | +  total_methods_head = head_stats[:typed_methods_size] + head_stats[:untyped_methods].size + head_stats[:partially_typed_methods].size  | 
 | 166 | +  typed_methods_percentage_base = (base_stats[:typed_methods_size] / total_methods_base.to_f * 100).round(2)  | 
 | 167 | +  typed_methods_percentage_head = (head_stats[:typed_methods_size] / total_methods_head.to_f * 100).round(2)  | 
 | 168 | + | 
 | 169 | +  create_summary(  | 
 | 170 | +    added: untyped_methods_added,  | 
 | 171 | +    removed: untyped_methods_removed,  | 
 | 172 | +    data_name: "untyped method",  | 
 | 173 | +    added_partially: partially_typed_methods_added,  | 
 | 174 | +    removed_partially: partially_typed_methods_removed,  | 
 | 175 | +    data_name_partially: "partially typed method",  | 
 | 176 | +    base_percentage: typed_methods_percentage_base,  | 
 | 177 | +    head_percentage: typed_methods_percentage_head,  | 
 | 178 | +    percentage_data_name: "typed method"  | 
 | 179 | +  )  | 
 | 180 | +end  | 
 | 181 | + | 
 | 182 | +def untyped_others_summary(head_stats, base_stats)  | 
 | 183 | +  untyped_others_added = head_stats[:untyped_others] - base_stats[:untyped_others]  | 
 | 184 | +  untyped_others_removed = base_stats[:untyped_others] - head_stats[:untyped_others]  | 
 | 185 | +  partially_typed_others_added = head_stats[:partially_typed_others] - base_stats[:partially_typed_others]  | 
 | 186 | +  partially_typed_others_removed = base_stats[:partially_typed_others] - head_stats[:partially_typed_others]  | 
 | 187 | +  total_others_base = base_stats[:typed_others_size] + base_stats[:untyped_others].size + base_stats[:partially_typed_others].size  | 
 | 188 | +  total_others_head = head_stats[:typed_others_size] + head_stats[:untyped_others].size + head_stats[:partially_typed_others].size  | 
 | 189 | +  typed_others_percentage_base = (base_stats[:typed_others_size] / total_others_base.to_f * 100).round(2)  | 
 | 190 | +  typed_others_percentage_head = (head_stats[:typed_others_size] / total_others_head.to_f * 100).round(2)  | 
 | 191 | + | 
 | 192 | +  create_summary(  | 
 | 193 | +    added: untyped_others_added,  | 
 | 194 | +    removed: untyped_others_removed,  | 
 | 195 | +    data_name: "untyped other declaration",  | 
 | 196 | +    added_partially: partially_typed_others_added,  | 
 | 197 | +    removed_partially: partially_typed_others_removed,  | 
 | 198 | +    data_name_partially: "partially typed other declaration",  | 
 | 199 | +    base_percentage: typed_others_percentage_base,  | 
 | 200 | +    head_percentage: typed_others_percentage_head,  | 
 | 201 | +    percentage_data_name: "typed other declaration"  | 
 | 202 | +  )  | 
 | 203 | +end  | 
 | 204 | + | 
 | 205 | +# Later we will make the CI fail if there's a regression in the typing stats  | 
 | 206 | +ignored_files_summary, _ignored_files_added = ignored_files_summary(head_stats, base_stats)  | 
 | 207 | +steep_ignore_summary, _steep_ignore_added = steep_ignore_summary(head_stats, base_stats)  | 
 | 208 | +untyped_methods_summary, untyped_methods_added = untyped_methods_summary(head_stats, base_stats)  | 
 | 209 | +untyped_others_summary, untyped_others_added = untyped_others_summary(head_stats, base_stats)  | 
 | 210 | +result = +""  | 
 | 211 | +result << ignored_files_summary if ignored_files_summary  | 
 | 212 | +if steep_ignore_summary || untyped_methods_summary || untyped_others_summary  | 
 | 213 | +  result << "*__Note__: Ignored files are excluded from the next sections.*\n\n"  | 
 | 214 | +end  | 
 | 215 | +result << steep_ignore_summary if steep_ignore_summary  | 
 | 216 | +result << untyped_methods_summary if untyped_methods_summary  | 
 | 217 | +result << untyped_others_summary if untyped_others_summary  | 
 | 218 | +if untyped_methods_added > 0 || untyped_others_added > 0  | 
 | 219 | +  result << "*If you believe a method or an attribute is rightfully untyped or partially typed, you can add `# untyped:accept` to the end of the line to remove it from the stats.*\n"  | 
 | 220 | +end  | 
 | 221 | +print result  | 
0 commit comments