From 9a9b215f0f83b15be8dc32fc66ef8897a7900243 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Wed, 13 May 2026 12:24:10 -0700 Subject: [PATCH 1/5] Initial changes --- .../lib/aws-sdk-code-generator.rb | 2 + .../aws-sdk-code-generator/code_builder.rb | 16 +- .../rbs/input_type_alias_collector.rb | 156 ++++++++++++++++++ .../rbs/keyword_argument_builder.rb | 16 +- .../views/rbs/client_class.rb | 3 + .../views/rbs/params.rb | 43 +++++ .../templates/rbs/params.mustache | 11 ++ 7 files changed, 240 insertions(+), 7 deletions(-) create mode 100644 build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb create mode 100644 build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb create mode 100644 build_tools/aws-sdk-code-generator/templates/rbs/params.mustache diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator.rb index f724b685700..5a8e67ac0cf 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator.rb @@ -83,6 +83,7 @@ require_relative 'aws-sdk-code-generator/rbs/error_list' require_relative 'aws-sdk-code-generator/rbs/method_signature' require_relative 'aws-sdk-code-generator/rbs/keyword_argument_builder' +require_relative 'aws-sdk-code-generator/rbs/input_type_alias_collector' require_relative 'aws-sdk-code-generator/rbs/resource_action' require_relative 'aws-sdk-code-generator/rbs/resource_association' require_relative 'aws-sdk-code-generator/rbs/resource_batch_action' @@ -90,6 +91,7 @@ require_relative 'aws-sdk-code-generator/rbs/waiter' require_relative 'aws-sdk-code-generator/views/rbs/async_client_class' require_relative 'aws-sdk-code-generator/views/rbs/client_class' +require_relative 'aws-sdk-code-generator/views/rbs/params' require_relative 'aws-sdk-code-generator/views/rbs/errors_module' require_relative 'aws-sdk-code-generator/views/rbs/resource_class' require_relative 'aws-sdk-code-generator/views/rbs/root_resource_class' diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb index 1dcfcb6fa61..c6041c512b6 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb @@ -114,8 +114,17 @@ def rbs_files(options = {}) prefix = options.fetch(:prefix, '') codegenerated_plugins = codegen_plugins(prefix) unless @service.h2_required_setting? - client_class = client_class_rbs(codegenerated_plugins) + collector = RBS::InputTypeAliasCollector.new(api: @service.api) + aliased_shapes = collector.shapes_to_alias + client_class = client_class_rbs(codegenerated_plugins, aliased_shapes) y.yield("#{prefix}/client.rbs", client_class.render) + if aliased_shapes.any? + y.yield("#{prefix}/params.rbs", Views::RBS::Params.new( + service_name: @service.name, + api: @service.api, + aliased_shapes: aliased_shapes + ).render) + end y.yield("#{prefix}/resource.rbs", Views::RBS::RootResourceClass.new( service_name: @service.name, client_class: client_class, @@ -197,7 +206,7 @@ def client_class(codegenerated_plugins) ).render end - def client_class_rbs(codegenerated_plugins) + def client_class_rbs(codegenerated_plugins, aliased_shapes) Views::RBS::ClientClass.new( service_name: @service.name, codegenerated_plugins: codegenerated_plugins, @@ -209,7 +218,8 @@ def client_class_rbs(codegenerated_plugins) protocol: @service.protocol, add_plugins: @service.add_plugins, remove_plugins: @service.remove_plugins, - protocol_settings: @service.protocol_settings + protocol_settings: @service.protocol_settings, + aliased_shapes: aliased_shapes ) end diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb new file mode 100644 index 00000000000..024e524760e --- /dev/null +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +module AwsSdkCodeGenerator + module RBS + # Collects structure shapes referenced more than once in input shape. + class InputTypeAliasCollector + def initialize(api:) + @api = api + @shape_usage_count = Hash.new(0) + @size_cache = {} + end + # Returns a topologically sorted array of shape names to render as type aliases. + # Leaf dependencies come first so aliases can reference each other. + # params.rbs uses aliases in this list to define RBS type aliases. + # client_class.rbs uses aliases in this list to deduplicate content. + def shapes_to_alias + count_shape_usage + aliased = @shape_usage_count.select do |shape_name, count| + shape = @api['shapes'][shape_name] + shape['type'] == 'structure' && + count > 1 && + rendered_rbs_line_count_heuristic(shape_name) > 5 + end.keys.to_set + topological_sort(aliased) + end + + private + + def topological_sort(shape_names) + sorted = [] + visited = Set.new + shape_names.each { |name| topo_visit(name, shape_names, visited, sorted) } + sorted + end + + def topo_visit(name, shape_names, visited, sorted) + return if visited.include?(name) + + visited << name + shape = @api['shapes'][name] + shape['members']&.each_value do |ref| + dep = ref['shape'] + topo_visit(dep, shape_names, visited, sorted) if shape_names.include?(dep) + end + sorted << name + end + + def count_shape_usage + @api['operations'].each_value do |op| + input_shape_name = op.dig('input', 'shape') + next unless input_shape_name + + walk_shape(input_shape_name, Set.new) + end + end + + def walk_shape(shape_name, ancestors) + return if ancestors.include?(shape_name) + + ancestors += [shape_name] + + shape = @api['shapes'][shape_name] + return unless shape && shape['type'] == 'structure' + + shape['members']&.each_value do |member_ref| + member_shape_name = member_ref['shape'] + member_shape = @api['shapes'][member_shape_name] + next unless member_shape + + case member_shape['type'] + when 'structure' + @shape_usage_count[member_shape_name] += 1 + walk_shape(member_shape_name, ancestors) + when 'list' + walk_list(member_shape, ancestors) + when 'map' + walk_map(member_shape, ancestors) + end + end + end + + def walk_list(list_shape, ancestors) + member_ref = list_shape['member'] + member_shape = @api['shapes'][member_ref['shape']] + return unless member_shape + + case member_shape['type'] + when 'structure' + @shape_usage_count[member_ref['shape']] += 1 + walk_shape(member_ref['shape'], ancestors) + when 'list' + walk_list(member_shape, ancestors) + when 'map' + walk_map(member_shape, ancestors) + end + end + + def walk_map(map_shape, ancestors) + value_ref = map_shape['value'] + value_shape = @api['shapes'][value_ref['shape']] + return unless value_shape + + case value_shape['type'] + when 'structure' + @shape_usage_count[value_ref['shape']] += 1 + walk_shape(value_ref['shape'], ancestors) + when 'list' + walk_list(value_shape, ancestors) + when 'map' + walk_map(value_shape, ancestors) + end + end + + def rendered_rbs_line_count_heuristic(shape_name, visited = Set.new) + return 0 if visited.include?(shape_name) + + # Cache results to deduplicate calculation for nested structure types that get used multiple times in the model. + @size_cache ||= {} + return @size_cache[shape_name] if @size_cache.key?(shape_name) + + visited += [shape_name] + shape = @api['shapes'][shape_name] + return @size_cache[shape_name] = 1 unless shape['members'] + + @size_cache[shape_name] = shape['members'].sum do |_, ref| + member_line_count(ref, visited) + end + end + + def member_line_count(ref, visited) + child = @api['shapes'][ref['shape']] + case child['type'] + # If it's a structure, add 2 lines then recurse (one line each for '{' and '}' in rendered RBS) + when 'structure' then 2 + rendered_rbs_line_count_heuristic(ref['shape'], visited) + when 'list' + member_shape = @api['shapes'][child['member']['shape']] + # If it's a list with structure member, add 2 lines & recurse. + # (one line each for 'Hash[::String, {' and '}]' in rendered RBS) + # If it's a list with primitive member, add 1 and return + # (e.g., renders as 1-liner, like 'list: Array[::String]') + member_shape['type'] == 'structure' ? 2 + + rendered_rbs_line_count_heuristic(child['member']['shape'], visited) : 1 + when 'map' + value_shape = @api['shapes'][child['value']['shape']] + # If it's a map with structure member, add 2 lines & recurse. + # (one line each for 'Hash[::String, {' and '}]' in rendered RBS) + # If it's a map with primitive member, add 1 and return + # (e.g., renders as 1-liner, like 'map: Hash[::String, ::String]') + value_shape['type'] == 'structure' ? 2 + + rendered_rbs_line_count_heuristic(child['value']['shape'], visited) : 1 + else 1 + end + end + end + end +end \ No newline at end of file diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb index 87b0843aeff..f9ac1dacdcc 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb @@ -8,11 +8,13 @@ class KeywordArgumentBuilder attr_reader :newline - def initialize(api:, shape:, newline:, options: {}) + def initialize(api:, shape:, newline:, options: {}, aliased_shapes: Set.new, alias_namespace: nil) @api = api @shape = shape @newline = newline @options = options + @aliased_shapes = aliased_shapes + @alias_namespace = alias_namespace end def format(indent: '') @@ -69,10 +71,16 @@ def struct_member(struct, member_name, member_ref, i, visited, keyword:) def ref_value(ref, i, visited) if visited.include?(ref['shape']) return "untyped" - else - visited = visited + [ref['shape']] end + # If this shape should be aliased, emit the alias reference + if @aliased_shapes.include?(ref['shape']) + alias_name = Underscore.underscore(ref['shape']) + return @alias_namespace ? "#{@alias_namespace}::#{alias_name}" : alias_name + end + + visited = visited + [ref['shape']] + s = shape(ref) case s['type'] when 'structure' @@ -115,7 +123,7 @@ def scalar_list(member_ref, i, visited) def complex_list(member_ref, i, visited) newline_indent = newline ? "\n#{i}" : "" - "Array[#{newline_indent}#{more_indent}#{ref_value(member_ref, i + more_indent, visited)},#{newline_indent}]" + "Array[#{newline_indent}#{more_indent}#{ref_value(member_ref, i + more_indent, visited)}#{newline_indent}]" end def complex?(ref) diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb index 778719cfd1a..dc81ba70c50 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb @@ -24,6 +24,7 @@ def initialize(options) @codegenerated_plugins = options.fetch(:codegenerated_plugins) @waiters = AwsSdkCodeGenerator::RBS::Waiter.build_list(api: @api, waiters:options.fetch(:waiters)) @protocol_settings = options.fetch(:protocol_settings, {}) + @aliased_shapes = options.fetch(:aliased_shapes, Set.new).to_set end # @return [String|nil] @@ -60,6 +61,8 @@ def operations api: @api, shape: input_shape, newline: true, + aliased_shapes: @aliased_shapes, + alias_namespace: 'Params', ) arguments = builder.format(indent: indent) include_required = input_shape["required"]&.empty?&.! diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb new file mode 100644 index 00000000000..40a3ac97ee0 --- /dev/null +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module AwsSdkCodeGenerator + module Views + module RBS + class Params < View + include Helper + + def initialize(options) + @service_name = options.fetch(:service_name) + @api = options.fetch(:api) + @aliased_shapes = options.fetch(:aliased_shapes) + end + + def generated_src_warning + GENERATED_SRC_WARNING + end + + def service_name + @service_name + end + + def type_aliases + aliased_set = @aliased_shapes.to_set + @aliased_shapes.map do |shape_name| + shape = @api['shapes'][shape_name] + builder = AwsSdkCodeGenerator::RBS::KeywordArgumentBuilder.new( + api: @api, + shape: shape, + newline: true, + aliased_shapes: aliased_set - [shape_name], + alias_namespace: 'Params', + ) + { + 'name' => Underscore.underscore(shape_name), + 'definition' => builder.struct(shape, ' ', []), + } + end + end + end + end + end +end diff --git a/build_tools/aws-sdk-code-generator/templates/rbs/params.mustache b/build_tools/aws-sdk-code-generator/templates/rbs/params.mustache new file mode 100644 index 00000000000..d6a81432d3a --- /dev/null +++ b/build_tools/aws-sdk-code-generator/templates/rbs/params.mustache @@ -0,0 +1,11 @@ +{{generated_src_warning}} +module Aws + module {{service_name}} + module Params + {{#type_aliases}} + type {{name}} = {{{definition}}} + + {{/type_aliases}} + end + end +end From 25ccb4ee1768c1ae85fe24ba40c53e8b345b50f9 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Wed, 13 May 2026 13:23:50 -0700 Subject: [PATCH 2/5] Unit tests for InputTypeAliasCollector --- .../input_type_alias_collector_spec.rb | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 build_tools/aws-sdk-code-generator/spec/aws-sdk-code-generator/input_type_alias_collector_spec.rb diff --git a/build_tools/aws-sdk-code-generator/spec/aws-sdk-code-generator/input_type_alias_collector_spec.rb b/build_tools/aws-sdk-code-generator/spec/aws-sdk-code-generator/input_type_alias_collector_spec.rb new file mode 100644 index 00000000000..1ba543ed3ea --- /dev/null +++ b/build_tools/aws-sdk-code-generator/spec/aws-sdk-code-generator/input_type_alias_collector_spec.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +module AwsSdkCodeGenerator + module RBS + describe InputTypeAliasCollector do + def api_with(shapes:, operations:) + { 'shapes' => shapes, 'operations' => operations } + end + + def large_structure + { + 'type' => 'structure', + 'members' => { + 'A' => { 'shape' => 'StringShape' }, + 'B' => { 'shape' => 'StringShape' }, + 'C' => { 'shape' => 'StringShape' }, + 'D' => { 'shape' => 'StringShape' }, + 'E' => { 'shape' => 'StringShape' }, + 'F' => { 'shape' => 'StringShape' } + } + } + end + + def string_shape + { 'type' => 'string' } + end + + describe '#shapes_to_alias' do + it 'returns empty when no shapes are reused' do + api = api_with( + shapes: { + 'InputShape' => { + 'type' => 'structure', + 'members' => { 'Name' => { 'shape' => 'StringShape' } } + }, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'InputShape' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to eq([]) + end + + it 'returns empty when a structure is reused but too small' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { 'Cfg' => { 'shape' => 'SmallStruct' } } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { 'Cfg' => { 'shape' => 'SmallStruct' } } + }, + 'SmallStruct' => { + 'type' => 'structure', + 'members' => { 'X' => { 'shape' => 'StringShape' } } + }, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to eq([]) + end + + it 'detects a structure used more than once with sufficient size' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { 'Config' => { 'shape' => 'LargeStruct' } } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { 'Config' => { 'shape' => 'LargeStruct' } } + }, + 'LargeStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to eq(['LargeStruct']) + end + + it 'detects structures nested inside lists' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { 'Items' => { 'shape' => 'ItemList' } } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { 'Items' => { 'shape' => 'ItemList' } } + }, + 'ItemList' => { 'type' => 'list', 'member' => { 'shape' => 'LargeStruct' } }, + 'LargeStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to eq(['LargeStruct']) + end + + it 'detects structures nested inside maps' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { 'Data' => { 'shape' => 'DataMap' } } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { 'Data' => { 'shape' => 'DataMap' } } + }, + 'DataMap' => { + 'type' => 'map', + 'key' => { 'shape' => 'StringShape' }, + 'value' => { 'shape' => 'LargeStruct' } + }, + 'LargeStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to eq(['LargeStruct']) + end + + it 'counts diamond references correctly' do + api = api_with( + shapes: { + 'InputShape' => { + 'type' => 'structure', + 'members' => { + 'B' => { 'shape' => 'StructB' }, + 'C' => { 'shape' => 'StructC' } + } + }, + 'StructB' => { + 'type' => 'structure', + 'members' => { 'D' => { 'shape' => 'LargeStruct' } } + }, + 'StructC' => { + 'type' => 'structure', + 'members' => { 'D' => { 'shape' => 'LargeStruct' } } + }, + 'LargeStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'InputShape' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to include('LargeStruct') + end + + it 'does not infinite loop on recursive shapes' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { 'Node' => { 'shape' => 'RecursiveStruct' } } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { 'Node' => { 'shape' => 'RecursiveStruct' } } + }, + 'RecursiveStruct' => { + 'type' => 'structure', + 'members' => { + 'A' => { 'shape' => 'StringShape' }, + 'B' => { 'shape' => 'StringShape' }, + 'C' => { 'shape' => 'StringShape' }, + 'D' => { 'shape' => 'StringShape' }, + 'E' => { 'shape' => 'StringShape' }, + 'F' => { 'shape' => 'StringShape' }, + 'Children' => { 'shape' => 'RecursiveList' } + } + }, + 'RecursiveList' => { 'type' => 'list', 'member' => { 'shape' => 'RecursiveStruct' } }, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + expect(collector.shapes_to_alias).to include('RecursiveStruct') + end + end + + describe 'topological ordering' do + it 'places leaf dependencies before their parents' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { + 'Parent' => { 'shape' => 'ParentStruct' }, + 'Leaf' => { 'shape' => 'LeafStruct' } + } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { + 'Parent' => { 'shape' => 'ParentStruct' }, + 'Leaf' => { 'shape' => 'LeafStruct' } + } + }, + 'ParentStruct' => { + 'type' => 'structure', + 'members' => { + 'Child' => { 'shape' => 'LeafStruct' }, + 'A' => { 'shape' => 'StringShape' }, + 'B' => { 'shape' => 'StringShape' }, + 'C' => { 'shape' => 'StringShape' }, + 'D' => { 'shape' => 'StringShape' }, + 'E' => { 'shape' => 'StringShape' } + } + }, + 'LeafStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + result = collector.shapes_to_alias + expect(result.index('LeafStruct')).to be < result.index('ParentStruct') + end + + it 'handles multi-level dependency chains' do + api = api_with( + shapes: { + 'Input1' => { + 'type' => 'structure', + 'members' => { + 'Top' => { 'shape' => 'TopStruct' }, + 'Mid' => { 'shape' => 'MidStruct' }, + 'Bot' => { 'shape' => 'BotStruct' } + } + }, + 'Input2' => { + 'type' => 'structure', + 'members' => { + 'Top' => { 'shape' => 'TopStruct' }, + 'Mid' => { 'shape' => 'MidStruct' }, + 'Bot' => { 'shape' => 'BotStruct' } + } + }, + 'TopStruct' => { + 'type' => 'structure', + 'members' => { + 'Mid' => { 'shape' => 'MidStruct' }, + 'A' => { 'shape' => 'StringShape' }, + 'B' => { 'shape' => 'StringShape' }, + 'C' => { 'shape' => 'StringShape' }, + 'D' => { 'shape' => 'StringShape' }, + 'E' => { 'shape' => 'StringShape' } + } + }, + 'MidStruct' => { + 'type' => 'structure', + 'members' => { + 'Bot' => { 'shape' => 'BotStruct' }, + 'A' => { 'shape' => 'StringShape' }, + 'B' => { 'shape' => 'StringShape' }, + 'C' => { 'shape' => 'StringShape' }, + 'D' => { 'shape' => 'StringShape' }, + 'E' => { 'shape' => 'StringShape' } + } + }, + 'BotStruct' => large_structure, + 'StringShape' => string_shape + }, + operations: { + 'Op1' => { 'input' => { 'shape' => 'Input1' } }, + 'Op2' => { 'input' => { 'shape' => 'Input2' } } + } + ) + collector = InputTypeAliasCollector.new(api: api) + result = collector.shapes_to_alias + expect(result.index('BotStruct')).to be < result.index('MidStruct') + expect(result.index('MidStruct')).to be < result.index('TopStruct') + end + end + end + end +end From 006919e67312d733066879aa54306f34c1f9bcd3 Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Thu, 14 May 2026 14:40:26 -0700 Subject: [PATCH 3/5] Address Juli's PR review comments --- .../aws-sdk-code-generator/code_builder.rb | 30 +++++++++++-------- .../rbs/input_type_alias_collector.rb | 1 - .../rbs/keyword_argument_builder.rb | 18 ++++++----- .../views/rbs/async_client_class.rb | 3 +- .../views/rbs/client_class.rb | 3 +- .../views/rbs/params.rb | 5 ++-- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb index c6041c512b6..ed9f8de5e48 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb @@ -113,18 +113,18 @@ def rbs_files(options = {}) Enumerator.new do |y| prefix = options.fetch(:prefix, '') codegenerated_plugins = codegen_plugins(prefix) + type_alias_collector = RBS::InputTypeAliasCollector.new(api: @service.api) + aliased_shapes = type_alias_collector.shapes_to_alias + if aliased_shapes.any? + y.yield("#{prefix}/params.rbs", Views::RBS::Params.new( + service_name: @service.name, + api: @service.api, + options: { aliased_shapes: aliased_shapes } + ).render) + end unless @service.h2_required_setting? - collector = RBS::InputTypeAliasCollector.new(api: @service.api) - aliased_shapes = collector.shapes_to_alias client_class = client_class_rbs(codegenerated_plugins, aliased_shapes) y.yield("#{prefix}/client.rbs", client_class.render) - if aliased_shapes.any? - y.yield("#{prefix}/params.rbs", Views::RBS::Params.new( - service_name: @service.name, - api: @service.api, - aliased_shapes: aliased_shapes - ).render) - end y.yield("#{prefix}/resource.rbs", Views::RBS::RootResourceClass.new( service_name: @service.name, client_class: client_class, @@ -133,7 +133,12 @@ def rbs_files(options = {}) paginators: @service.paginators ).render) end - y.yield("#{prefix}/async_client.rbs", async_client_class_rbs(codegenerated_plugins).render) if @service.h2_setting? + if @service.h2_setting? + y.yield("#{prefix}/async_client.rbs", async_client_class_rbs( + codegenerated_plugins, + aliased_shapes + ).render) + end y.yield("#{prefix}/errors.rbs", Views::RBS::ErrorsModule.new(service: @service).render) if @waiters y.yield("#{prefix}/waiters.rbs", Views::RBS::WaitersModule.new( @@ -243,7 +248,7 @@ def async_client_class(codegenerated_plugins) ).render end - def async_client_class_rbs(codegenerated_plugins) + def async_client_class_rbs(codegenerated_plugins, aliased_shapes) Views::RBS::AsyncClientClass.new( service_name: @service.name, codegenerated_plugins: codegenerated_plugins, @@ -255,7 +260,8 @@ def async_client_class_rbs(codegenerated_plugins) add_plugins: @service.add_plugins, remove_plugins: @service.remove_plugins, protocol_settings: @service.protocol_settings, - async_client: true + async_client: true, + aliased_shapes: aliased_shapes ) end diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb index 024e524760e..a8e7688b517 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb @@ -115,7 +115,6 @@ def rendered_rbs_line_count_heuristic(shape_name, visited = Set.new) return 0 if visited.include?(shape_name) # Cache results to deduplicate calculation for nested structure types that get used multiple times in the model. - @size_cache ||= {} return @size_cache[shape_name] if @size_cache.key?(shape_name) visited += [shape_name] diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb index f9ac1dacdcc..bc4c437b0fd 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb @@ -8,13 +8,11 @@ class KeywordArgumentBuilder attr_reader :newline - def initialize(api:, shape:, newline:, options: {}, aliased_shapes: Set.new, alias_namespace: nil) + def initialize(api:, shape:, newline:, options: {}) @api = api @shape = shape @newline = newline @options = options - @aliased_shapes = aliased_shapes - @alias_namespace = alias_namespace end def format(indent: '') @@ -26,6 +24,12 @@ def format(indent: '') result.join(joint) end + def format_as_alias(indent: '') + struct(@shape, indent, []) + end + + private + def struct(struct_shape, i, visited) members_str = struct_members(struct_shape, i, visited, keyword: false) result = ["{"] @@ -69,14 +73,12 @@ def struct_member(struct, member_name, member_ref, i, visited, keyword:) end def ref_value(ref, i, visited) - if visited.include?(ref['shape']) - return "untyped" - end + return "untyped" if visited.include?(ref['shape']) # If this shape should be aliased, emit the alias reference - if @aliased_shapes.include?(ref['shape']) + if @options[:aliased_shapes]&.include?(ref['shape']) alias_name = Underscore.underscore(ref['shape']) - return @alias_namespace ? "#{@alias_namespace}::#{alias_name}" : alias_name + return "Params::#{alias_name}" end visited = visited + [ref['shape']] diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/async_client_class.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/async_client_class.rb index 68857bbfed8..c6dd618db75 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/async_client_class.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/async_client_class.rb @@ -25,6 +25,7 @@ def initialize(options) @plugins = PluginList.new(options) @codegenerated_plugins = options.fetch(:codegenerated_plugins) @protocol_settings = options.fetch(:protocol_settings, {}) + @aliased_shapes = options.fetch(:aliased_shapes, Set.new).to_set end # @return [String|nil] @@ -68,7 +69,7 @@ def operations api: @api, shape: input_shape, newline: true, - options: options + options: options.merge(aliased_shapes: @aliased_shapes) ) arguments = builder.format(indent: indent) include_required = input_shape['required']&.empty?&.! diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb index dc81ba70c50..2177f4db9dc 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/client_class.rb @@ -61,8 +61,7 @@ def operations api: @api, shape: input_shape, newline: true, - aliased_shapes: @aliased_shapes, - alias_namespace: 'Params', + options: { aliased_shapes: @aliased_shapes } ) arguments = builder.format(indent: indent) include_required = input_shape["required"]&.empty?&.! diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb index 40a3ac97ee0..b267ad4afbf 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb @@ -28,12 +28,11 @@ def type_aliases api: @api, shape: shape, newline: true, - aliased_shapes: aliased_set - [shape_name], - alias_namespace: 'Params', + options: { aliased_shapes: aliased_set - [shape_name] } ) { 'name' => Underscore.underscore(shape_name), - 'definition' => builder.struct(shape, ' ', []), + 'definition' => builder.format_as_alias(indent: ' '), } end end From 77bbfa66ca60952a3bbc9a07b6fc18f84ae7b02d Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Thu, 14 May 2026 15:16:13 -0700 Subject: [PATCH 4/5] Address CodeQL scan warnings & compile error --- .../lib/aws-sdk-code-generator/code_builder.rb | 2 +- .../lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb | 2 +- .../lib/aws-sdk-code-generator/views/rbs/params.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb index ed9f8de5e48..95e54308c9f 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/code_builder.rb @@ -119,7 +119,7 @@ def rbs_files(options = {}) y.yield("#{prefix}/params.rbs", Views::RBS::Params.new( service_name: @service.name, api: @service.api, - options: { aliased_shapes: aliased_shapes } + aliased_shapes: aliased_shapes ).render) end unless @service.h2_required_setting? diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb index bc4c437b0fd..475da3d3c0e 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/keyword_argument_builder.rb @@ -77,7 +77,7 @@ def ref_value(ref, i, visited) # If this shape should be aliased, emit the alias reference if @options[:aliased_shapes]&.include?(ref['shape']) - alias_name = Underscore.underscore(ref['shape']) + alias_name = underscore(ref['shape']) return "Params::#{alias_name}" end diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb index b267ad4afbf..a4be3b20c42 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/views/rbs/params.rb @@ -31,7 +31,7 @@ def type_aliases options: { aliased_shapes: aliased_set - [shape_name] } ) { - 'name' => Underscore.underscore(shape_name), + 'name' => underscore(shape_name), 'definition' => builder.format_as_alias(indent: ' '), } end From cad391a807ba08b38b5d06352ac171863454e84b Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Tue, 19 May 2026 09:56:50 -0700 Subject: [PATCH 5/5] Use constant for min lines threshold for alias & add newline at the end of file. --- .../rbs/input_type_alias_collector.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb index a8e7688b517..420ff10f852 100644 --- a/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb +++ b/build_tools/aws-sdk-code-generator/lib/aws-sdk-code-generator/rbs/input_type_alias_collector.rb @@ -4,11 +4,14 @@ module AwsSdkCodeGenerator module RBS # Collects structure shapes referenced more than once in input shape. class InputTypeAliasCollector + MIN_LINES_THRESHOLD_FOR_ALIAS = 5 + def initialize(api:) @api = api @shape_usage_count = Hash.new(0) @size_cache = {} end + # Returns a topologically sorted array of shape names to render as type aliases. # Leaf dependencies come first so aliases can reference each other. # params.rbs uses aliases in this list to define RBS type aliases. @@ -19,7 +22,7 @@ def shapes_to_alias shape = @api['shapes'][shape_name] shape['type'] == 'structure' && count > 1 && - rendered_rbs_line_count_heuristic(shape_name) > 5 + rendered_rbs_line_count_heuristic(shape_name) > MIN_LINES_THRESHOLD_FOR_ALIAS end.keys.to_set topological_sort(aliased) end @@ -152,4 +155,4 @@ def member_line_count(ref, visited) end end end -end \ No newline at end of file +end