Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 74 additions & 8 deletions .github/scripts/typing_stats_compare.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,64 @@
head_stats = JSON.parse(File.read(ENV["CURRENT_STATS_PATH"]), symbolize_names: true)
base_stats = JSON.parse(File.read(ENV["BASE_STATS_PATH"]), symbolize_names: true)

# Synthesizes constant names from every part of an RBS file path.
## Example: "path/to/resolver.rbs" -> ["Path", "To", "Resolver"]
def constant_names_from_path(path)
path.delete_suffix(".rbs").split("/").map do |part|
part.split("_").map(&:capitalize).join
end
end

# Parse renames from Git.
RENAMED_PATHS = File.readlines(ENV["RENAMED_PATHS_PATH"], chomp: true).each_with_object({}) do |line, result|
_, base_path, head_path = line.split("\t", 3)
result[head_path] = base_path
end

# Look up for renamed constant names.
# Example: on rename from base "base.rbs" to head "head.rbs", "Head" should compare as "Base".
# Same for nested constant paths: ['NewPath', 'NewPath::Core'] compares as ['BasePath', 'BasePath::Core'].
RENAMED_CONSTANTS = RENAMED_PATHS.each_with_object({}) do |(head_path, base_path), result|
result[head_path] = constant_names_from_path(head_path).zip(constant_names_from_path(base_path))
end

# Normalizes comparison keys so findings follow renames.
# Example: on rename from base "base.rbs" to head "head.rbs", both paths compare as "base.rbs".
def comparison_key(item)
key = item[:comparison_key].dup
path = key[:path]
base_path = RENAMED_PATHS[path]

if key[:constant_path] && RENAMED_CONSTANTS[path]
key[:constant_path] = key[:constant_path].map do |owner|
owner.split("::").map { |part| RENAMED_CONSTANTS[path].assoc(part)&.last || part }.join("::")
end
end

# Normalize key as base path.
key[:path] = base_path || path
key
end

# Compares keyed findings while preserving duplicate counts.
def multiset_comparison(head_items, base_items)
unmatched_base = base_items.group_by { |item| comparison_key(item) }
added = []

head_items.each do |item|
base_matches = unmatched_base[comparison_key(item)]
if base_matches&.any?
# Consume one matching base finding so duplicate findings are counted independently.
base_matches.shift
else
added << item
end
end

removed = unmatched_base.values.flatten
[added, removed]
end

def format_for_code_block(data)
data.map do |item|
formatted_string = +"#{item[:path]}:#{item[:line]}"
Expand Down Expand Up @@ -157,10 +215,14 @@ def steep_ignore_summary(head_stats, base_stats)
end

def untyped_methods_summary(head_stats, base_stats)
untyped_methods_added = head_stats[:untyped_methods] - base_stats[:untyped_methods]
untyped_methods_removed = base_stats[:untyped_methods] - head_stats[:untyped_methods]
partially_typed_methods_added = head_stats[:partially_typed_methods] - base_stats[:partially_typed_methods]
partially_typed_methods_removed = base_stats[:partially_typed_methods] - head_stats[:partially_typed_methods]
untyped_methods_added, untyped_methods_removed = multiset_comparison(
head_stats[:untyped_methods],
base_stats[:untyped_methods]
)
partially_typed_methods_added, partially_typed_methods_removed = multiset_comparison(
head_stats[:partially_typed_methods],
base_stats[:partially_typed_methods]
)
total_methods_base = base_stats[:typed_methods_size] + base_stats[:untyped_methods].size + base_stats[:partially_typed_methods].size
total_methods_head = head_stats[:typed_methods_size] + head_stats[:untyped_methods].size + head_stats[:partially_typed_methods].size
typed_methods_percentage_base = (base_stats[:typed_methods_size] / total_methods_base.to_f * 100).round(2)
Expand All @@ -180,10 +242,14 @@ def untyped_methods_summary(head_stats, base_stats)
end

def untyped_others_summary(head_stats, base_stats)
untyped_others_added = head_stats[:untyped_others] - base_stats[:untyped_others]
untyped_others_removed = base_stats[:untyped_others] - head_stats[:untyped_others]
partially_typed_others_added = head_stats[:partially_typed_others] - base_stats[:partially_typed_others]
partially_typed_others_removed = base_stats[:partially_typed_others] - head_stats[:partially_typed_others]
untyped_others_added, untyped_others_removed = multiset_comparison(
head_stats[:untyped_others],
base_stats[:untyped_others]
)
partially_typed_others_added, partially_typed_others_removed = multiset_comparison(
head_stats[:partially_typed_others],
base_stats[:partially_typed_others]
)
total_others_base = base_stats[:typed_others_size] + base_stats[:untyped_others].size + base_stats[:partially_typed_others].size
total_others_head = head_stats[:typed_others_size] + head_stats[:untyped_others].size + head_stats[:partially_typed_others].size
typed_others_percentage_base = (base_stats[:typed_others_size] / total_others_base.to_f * 100).round(2)
Expand Down
42 changes: 32 additions & 10 deletions .github/scripts/typing_stats_compute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@

total_files_size = Dir.glob("#{project.base_dir}/lib/**/*.rb").size

def stats_item(path, line, line_content, comparison_key)
{
path: path.to_s,
line: line,
line_content: line_content,
comparison_key: comparison_key
}.compact
end

# Include the enclosing namespace so equivalent RBS declarations in different owners stay distinct.
def rbs_item(path, line, source, constant_path)
stats_item(path, line, source, {type: "rbs_declaration", path: path.to_s, constant_path: constant_path, source: source})
end

# steep:ignore comments stats
ignore_comments = loader.each_path_in_patterns(datadog_target.source_pattern).each_with_object([]) do |path, result|
buffer = ::Parser::Source::Buffer.new(path.to_s, 1, source: path.read)
Expand All @@ -55,24 +69,28 @@
end
end

def ast_traversal(declarations, result = {})
# Collects declarations with their constant path for stable comparison keys.
# @param declarations [Array<RBS::AST::Declarations::Base, RBS::AST::Members::Base>]
# @param result [Hash] accumulated methods and other declarations
# @param constant_path [Array<String>] enclosing RBS constant path for the current item, e.g. ["::Datadog", "::Datadog::Core"]
def ast_traversal(declarations, result = {}, constant_path = [])
result[:methods] ||= []
result[:others] ||= []
declarations.each do |declaration|
case declaration
when ::RBS::AST::Declarations::Module,
::RBS::AST::Declarations::Class,
::RBS::AST::Declarations::Interface
ast_traversal(declaration.members, result)
ast_traversal(declaration.members, result, constant_path + [declaration.name.to_s])
when ::RBS::AST::Declarations::TypeAlias,
::RBS::AST::Declarations::Constant,
::RBS::AST::Declarations::Global,
::RBS::AST::Members::Var,
::RBS::AST::Members::Attribute
result[:others] << declaration
result[:others] << {declaration: declaration, constant_path: constant_path}
# Only this one does not have a type field
when ::RBS::AST::Members::MethodDefinition
result[:methods] << declaration
result[:methods] << {declaration: declaration, constant_path: constant_path}
end
end
result
Expand Down Expand Up @@ -177,7 +195,9 @@ def is_typed?(type, initialize: false)
_, _directives, declarations = ::RBS::Parser.parse_signature(buffer)
filtered_declarations = ast_traversal(declarations)

filtered_declarations[:methods].each do |method|
filtered_declarations[:methods].each do |entry|
method = entry[:declaration]

# Skip definitions with last comment line being `untyped:accept`
if method.comment&.string&.end_with?("untyped:accept\n")
typed_methods_size += 1
Expand All @@ -192,13 +212,15 @@ def is_typed?(type, initialize: false)
when :typed
typed_methods_size += 1
when :untyped
untyped_methods << {path: sig_path.to_s, line: method.location.start_line, line_content: method.location.source}
untyped_methods << rbs_item(sig_path, method.location.start_line, method.location.source, entry[:constant_path])
when :partial
partially_typed_methods << {path: sig_path.to_s, line: method.location.start_line, line_content: method.location.source}
partially_typed_methods << rbs_item(sig_path, method.location.start_line, method.location.source, entry[:constant_path])
end
end

filtered_declarations[:others].each do |declaration|
filtered_declarations[:others].each do |entry|
declaration = entry[:declaration]

# Skip definitions with last comment line being `untyped:accept`
if declaration.comment&.string&.end_with?("untyped:accept\n")
typed_others_size += 1
Expand All @@ -209,9 +231,9 @@ def is_typed?(type, initialize: false)
when :typed, nil
typed_others_size += 1
when :untyped
untyped_others << {path: sig_path.to_s, line: declaration.location.start_line, line_content: declaration.location.source}
untyped_others << rbs_item(sig_path, declaration.location.start_line, declaration.location.source, entry[:constant_path])
when :partial
partially_typed_others << {path: sig_path.to_s, line: declaration.location.start_line, line_content: declaration.location.source}
partially_typed_others << rbs_item(sig_path, declaration.location.start_line, declaration.location.source, entry[:constant_path])
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/typing-stats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
- name: Checkout current branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Set up Ruby
Expand All @@ -67,6 +68,9 @@ jobs:
mkdir -p "${{ github.workspace }}/tmp"
ruby ${{ runner.temp }}/typing_stats_compute.rb >> "${{ runner.temp }}/typing-stats-current.json"

- name: Collect renamed paths
run: git diff --find-renames --diff-filter=R --name-status ${{ github.event.pull_request.base.sha }} HEAD >> "${{ runner.temp }}/typing-stats-renamed-paths.txt"

- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand All @@ -84,6 +88,7 @@ jobs:
env:
CURRENT_STATS_PATH: ${{ runner.temp }}/typing-stats-current.json
BASE_STATS_PATH: ${{ runner.temp }}/typing-stats-base.json
RENAMED_PATHS_PATH: ${{ runner.temp }}/typing-stats-renamed-paths.txt
run: ruby ${{ runner.temp }}/typing_stats_compare.rb >> "${{ runner.temp }}/typing-stats-compare.md"

- name: Write comment
Expand Down
2 changes: 1 addition & 1 deletion Matrixfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'core-old' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 4.0'
},
# Rubocop not included in Ruby < 2.7 gemfiles
'custom_cop' => {
'tooling' => {
'' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 4.0'
},
'core_with_libdatadog_api' => {
Expand Down
13 changes: 10 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ end
desc 'Run RSpec'
namespace :spec do
# REMINDER: If adding a new task here, make sure also add it to the `Matrixfile`
task all: [:main, :benchmark, :custom_cop,
task all: [:main, :benchmark, :tooling,
:graphql, :graphql_unified_trace_patcher, :graphql_trace_patcher, :graphql_tracing_patcher,
:rails, :railsredis, :railsredis_activesupport, :railsactivejob,
:elasticsearch, :http, :redis, :sidekiq, :sinatra, :hanami, :hanami_autoinstrument,
Expand All @@ -99,7 +99,10 @@ namespace :spec do
desc '' # "Explicitly hiding from `rake -T`"
RSpec::Core::RakeTask.new(:main) do |t, args|
t.pattern = 'spec/**/*_spec.rb'

# Add new entries as new lines, for readable diffs.
t.exclude_pattern = 'spec/**/{appsec/integration,contrib,benchmark,redis,auto_instrument,opentelemetry,open_feature,profiling,error_tracking,rubocop,ai_guard}/**/*_spec.rb,' \
' spec/tooling/**/*_spec.rb,' \
' spec/**/{auto_instrument,opentelemetry,process,ai_guard}_spec.rb,' \
' spec/**/*_rails_spec.rb,' \
' spec/datadog/core/environment/execution_spec.rb,' \
Expand All @@ -115,8 +118,12 @@ namespace :spec do
t.rspec_opts = args.to_a.join(' ')
end

RSpec::Core::RakeTask.new(:custom_cop) do |t, args|
t.pattern = 'spec/rubocop/**/*_spec.rb'
desc 'Scripts and dev tools'
RSpec::Core::RakeTask.new(:tooling) do |t, args|
t.pattern = [
'spec/tooling/**/*_spec.rb',
'spec/rubocop/**/*_spec.rb'
]
t.rspec_opts = args.to_a.join(' ')
end

Expand Down
Loading
Loading