|
| 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