Skip to content

Commit 2f20b9a

Browse files
authored
[APMAPI-1618] Only show diff in typing stats (#4976)
1 parent c5e3981 commit 2f20b9a

File tree

3 files changed

+277
-78
lines changed

3 files changed

+277
-78
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

.github/scripts/typing_stats.rb renamed to .github/scripts/typing_stats_compute.rb

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
#!/usr/bin/env ruby
22

3-
require 'steep'
4-
require 'parser/ruby25'
5-
require 'json'
3+
require "steep"
4+
require "parser/ruby25"
5+
require "json"
66

77
METHOD_AND_PARAM_NAME = /(?:\w*|`[^`]+`)/
88
PARAMETER = /(?:\*{1,2})?\s*(?:\??\s*untyped\s*\??\s*|\??#{METHOD_AND_PARAM_NAME}:\s*untyped\s*\??)\s*#{METHOD_AND_PARAM_NAME}/
99
PARAMETERS = /\(\s*(?:\?|(?:(?:#{PARAMETER})\s*(?:,\s*(?:#{PARAMETER})\s*)*)?)\s*\)/
1010
PROTOTYPE_INITIALIZE = /\s*(?:public|private)?\s*def\s+initialize:\s*#{PARAMETERS}(?:\s*\??\{\s*#{PARAMETERS}\s*->\s*untyped\s*\})?\s*->\s*void/
1111
PROTOTYPE_METHOD = /\s*(?:public|private)?\s*def\s+(?:self\??\.)?(?:[^\s]+):\s*#{PARAMETERS}(?:\s*\??\{\s*#{PARAMETERS}\s*->\s*untyped\s*\})?\s*->\s*untyped/
1212

13-
# TODO: Find untyped/partially typed attributes, instance variables, class variables, constants
14-
15-
steepfile_path = Pathname(ENV['STEEPFILE_PATH'])
13+
steepfile_path = Pathname(ENV["STEEPFILE_PATH"])
1614
project = Steep::Project.new(steepfile_path: steepfile_path).tap do |project|
1715
Steep::Project::DSL.parse(project, steepfile_path.read, filename: steepfile_path.to_s)
1816
end
1917
datadog_target = project.targets&.find { |target| target.name == :datadog }
2018
loader = ::Steep::Services::FileLoader.new(base_dir: project.base_dir)
2119

22-
ignored_paths = datadog_target&.source_pattern&.ignores
20+
ignored_paths_with_folders = datadog_target&.source_pattern&.ignores
21+
22+
ignored_files = ignored_paths_with_folders.each_with_object([]) do |ignored_path, result|
23+
# If the ignored path is a folder, add all the .rb files in the folder to the ignored paths
24+
if ignored_path.end_with?("/")
25+
result.push(*Dir.glob(ignored_path + "**/*.rb"))
26+
else
27+
result.push(ignored_path)
28+
end
29+
end
2330

2431
# List signature files that are not related to ignored files
2532
signature_paths_with_ignored_files = loader.each_path_in_patterns(datadog_target.signature_pattern)
2633
signature_paths = signature_paths_with_ignored_files.reject do |sig_path|
27-
# replace sig/ with lib/ and .rbs with .rb
28-
corresponding_lib_file = sig_path.to_s.sub(/^sig/, 'lib').sub(/\.rbs$/, '.rb')
29-
ignored_paths.any? do |ignored|
30-
if ignored.end_with?('/')
34+
corresponding_lib_file = sig_path.to_s.sub(/^sig/, "lib").sub(/\.rbs$/, ".rb")
35+
ignored_paths_with_folders.any? do |ignored|
36+
if ignored.end_with?("/")
3137
# Directory ignore - check if signature file is inside this directory
3238
corresponding_lib_file.start_with?(ignored)
3339
else
@@ -37,14 +43,6 @@
3743
end
3844
end
3945

40-
# Ignored files stats
41-
ignored_files_size = ignored_paths.inject(0) do |result, path|
42-
if path.end_with?('/')
43-
result + Dir.glob(path + '**/*.rb').size
44-
else
45-
result + 1
46-
end
47-
end
4846
total_files_size = Dir.glob("#{project.base_dir}/lib/**/*.rb").size
4947

5048
# steep:ignore comments stats
@@ -121,10 +119,7 @@
121119

122120
resulting_stats = {
123121
total_files_size: total_files_size,
124-
ignored_files: {
125-
size: ignored_files_size, # Required as we don't list all ignored files, but only their paths
126-
paths: ignored_paths
127-
},
122+
ignored_files: ignored_files,
128123

129124
steep_ignore_comments: ignore_comments,
130125

0 commit comments

Comments
 (0)