rigortype 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +3 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +146 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +30 -9
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +5 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +52 -1
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module SigGen
5
+ # Maps a source `.rb` file to its target `.rbs` sig file
6
+ # under the project's signature tree.
7
+ #
8
+ # ADR-14 § "Output layout":
9
+ # - `--write` MUST NOT touch files outside
10
+ # `configuration.signature_paths` (default `sig/`).
11
+ # - The first slice supports one source file → one RBS
12
+ # file; multi-class files emit one RBS containing both
13
+ # classes (handled by the {Writer}, not here).
14
+ #
15
+ # The mapping convention mirrors the Ruby community
16
+ # default: strip the source root prefix (the first entry
17
+ # of `configuration.paths`, typically `"lib"`), swap the
18
+ # extension, and place the result under the first entry of
19
+ # `configuration.signature_paths` (typically `"sig"`).
20
+ #
21
+ # ADR-14 follow-up: when a class is already declared in
22
+ # an existing consolidated sig file (e.g. `sig/rigor/type.rbs`
23
+ # holds all `Rigor::Type::*` classes), the optional
24
+ # `LayoutIndex` re-routes the target to that file so the
25
+ # writer updates the consolidated declaration instead of
26
+ # creating a duplicate at the 1:1 mirror path.
27
+ #
28
+ # When the source path is not under any configured source
29
+ # root (e.g. files supplied directly on the CLI from
30
+ # outside `lib/`), the full relative path is preserved
31
+ # under the sig root.
32
+ class PathMapper
33
+ # @param configuration [Rigor::Configuration]
34
+ # @param project_root [String, Pathname] (defaults to `Dir.pwd`)
35
+ # @param layout_index [LayoutIndex, nil] optional class
36
+ # → existing sig file index; routes the target to the
37
+ # consolidated file when the class is already declared.
38
+ def initialize(configuration:, project_root: Dir.pwd, layout_index: nil)
39
+ @configuration = configuration
40
+ @project_root = Pathname(project_root)
41
+ @layout_index = layout_index
42
+ end
43
+
44
+ # @param source_path [String]
45
+ # @param class_name [String, nil] fully-qualified Ruby
46
+ # class name. When supplied and matched by the
47
+ # `LayoutIndex`, the consolidated sig file's path is
48
+ # returned instead of the 1:1 mirror.
49
+ # @return [Pathname] absolute path of the target `.rbs`
50
+ # file for the candidate.
51
+ def target_for(source_path, class_name: nil)
52
+ existing = existing_target_for(class_name)
53
+ return existing if existing
54
+
55
+ rel_to_root = source_relative_to_root(source_path)
56
+ stripped = strip_source_root(rel_to_root)
57
+ sig_root_dir / "#{stripped.sub_ext('')}.rbs"
58
+ end
59
+
60
+ def existing_target_for(class_name)
61
+ return nil if class_name.nil? || @layout_index.nil?
62
+
63
+ @layout_index.file_for(class_name)
64
+ end
65
+
66
+ # The directory `--write` is allowed to create / modify.
67
+ # Used by callers to assert the target stays inside the
68
+ # configured signature tree before touching the disk.
69
+ def sig_root_dir
70
+ @sig_root_dir ||= @project_root / sig_root_name
71
+ end
72
+
73
+ private
74
+
75
+ def source_relative_to_root(source_path)
76
+ path = Pathname(source_path)
77
+ return path unless path.absolute?
78
+
79
+ # Both sides go through realpath so macOS `/tmp` vs
80
+ # `/private/tmp` (and any other symlinked project
81
+ # root) compare cleanly.
82
+ path.realpath.relative_path_from(@project_root.realpath)
83
+ rescue ArgumentError, Errno::ENOENT
84
+ path
85
+ end
86
+
87
+ def strip_source_root(rel_path)
88
+ source_root = source_root_name
89
+ return rel_path if source_root.nil?
90
+
91
+ first = rel_path.each_filename.first
92
+ return rel_path unless first == source_root
93
+
94
+ components = rel_path.each_filename.drop(1)
95
+ components.empty? ? Pathname("") : Pathname(components.join(File::SEPARATOR))
96
+ end
97
+
98
+ # `Configuration` resolves `paths:` and `signature_paths:`
99
+ # to absolute Strings. We only need the trailing basename
100
+ # for the mapping (`/abs/lib` → `lib`, `/abs/app` → `app`).
101
+ def source_root_name
102
+ @source_root_name ||= begin
103
+ path = @configuration.paths.first
104
+ path.nil? || path.empty? ? nil : Pathname(path).basename.to_s
105
+ end
106
+ end
107
+
108
+ def sig_root_name
109
+ @sig_root_name ||= begin
110
+ first_sig = Array(@configuration.signature_paths).first
111
+ first_sig.nil? ? "sig" : Pathname(first_sig).basename.to_s
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "classification"
6
+
7
+ module Rigor
8
+ module SigGen
9
+ # Output formatter for `rigor sig-gen`.
10
+ #
11
+ # Supports three modes:
12
+ # - `:print` (default) — RBS skeletons grouped by source
13
+ # file and class declaration, ready for the user to
14
+ # paste into `sig/<path>.rbs`.
15
+ # - `:diff` — a unified-style diff comparing the existing
16
+ # RBS spelling (if any) against the inferred spelling.
17
+ # The MVP renders a minimal "- declared / + inferred"
18
+ # block; full per-file diffing arrives with slice 2's
19
+ # `--write` merge.
20
+ # - `:json` — machine-readable payload with the same
21
+ # classification table as `:print`.
22
+ class Renderer
23
+ def initialize(out:)
24
+ @out = out
25
+ end
26
+
27
+ # @param candidates [Array<MethodCandidate>]
28
+ # @param mode [:print, :diff]
29
+ # @param format [String] "text" or "json"
30
+ # @param selection [Array<Symbol>] subset of
31
+ # {Classification} constants to include; an empty
32
+ # array means "all emittable classifications".
33
+ def render(candidates:, mode:, format:, selection:)
34
+ filtered = filter(candidates, selection)
35
+
36
+ case format
37
+ when "json" then render_json(filtered)
38
+ when "text"
39
+ mode == :diff ? render_diff(filtered) : render_print(filtered)
40
+ else
41
+ raise ArgumentError, "unsupported format: #{format}"
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ EMITTABLE = [Classification::NEW_FILE,
48
+ Classification::NEW_METHOD,
49
+ Classification::TIGHTER_RETURN].freeze
50
+ private_constant :EMITTABLE
51
+
52
+ def filter(candidates, selection)
53
+ active = selection.empty? ? EMITTABLE : selection
54
+ candidates.select { |c| active.include?(c.classification) }
55
+ end
56
+
57
+ def render_print(candidates)
58
+ if candidates.empty?
59
+ @out.puts("No candidates")
60
+ return
61
+ end
62
+
63
+ grouped = candidates.group_by(&:path)
64
+ grouped.each do |path, items|
65
+ @out.puts("# #{path}")
66
+ render_classes(items)
67
+ @out.puts
68
+ end
69
+ end
70
+
71
+ def render_classes(items)
72
+ items.group_by(&:class_name).each do |class_name, methods|
73
+ @out.puts("class #{class_name}")
74
+ methods.each do |candidate|
75
+ tag = case candidate.classification
76
+ when Classification::NEW_METHOD then "[new]"
77
+ when Classification::NEW_FILE then "[new-file]"
78
+ when Classification::TIGHTER_RETURN
79
+ "[tighter, was: #{candidate.declared_return_rbs}]"
80
+ end
81
+ @out.puts(" # #{tag}")
82
+ @out.puts(" #{candidate.rbs}")
83
+ end
84
+ @out.puts("end")
85
+ end
86
+ end
87
+
88
+ def render_diff(candidates)
89
+ if candidates.empty?
90
+ @out.puts("No candidates")
91
+ return
92
+ end
93
+
94
+ candidates.each do |candidate|
95
+ @out.puts("--- #{candidate.path}: #{candidate.class_name}##{candidate.method_name}")
96
+ declared = candidate.declared_return_rbs
97
+ @out.puts("- def #{candidate.method_name}: () -> #{declared}") if declared
98
+ @out.puts("+ #{candidate.rbs}")
99
+ @out.puts
100
+ end
101
+ end
102
+
103
+ def render_json(candidates)
104
+ payload = { candidates: candidates.map(&:to_h) }
105
+ @out.puts(JSON.pretty_generate(payload))
106
+ end
107
+
108
+ public
109
+
110
+ # Renders the per-source-file outcomes of a `--write`
111
+ # run. Distinct from {#render} because the write
112
+ # path's reporting surface is action-oriented (created
113
+ # / updated / skipped) rather than candidate-oriented.
114
+ def render_write(results:, format:)
115
+ case format
116
+ when "json" then render_write_json(results)
117
+ when "text" then render_write_text(results)
118
+ else raise ArgumentError, "unsupported format: #{format}"
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def render_write_text(results)
125
+ if results.all? { |r| r.action == :noop }
126
+ @out.puts("No changes")
127
+ return
128
+ end
129
+
130
+ results.each do |result|
131
+ case result.action
132
+ when :created then render_write_created(result)
133
+ when :updated then render_write_updated(result)
134
+ when :skipped_outside_sig_root then render_write_skipped(result)
135
+ end
136
+ end
137
+ end
138
+
139
+ def render_write_created(result)
140
+ @out.puts("created #{result.target_path} (#{result.applied.size} method(s))")
141
+ end
142
+
143
+ def render_write_updated(result)
144
+ @out.puts("updated #{result.target_path} (+#{result.applied.size}, " \
145
+ "skipped #{result.skipped.size} user-authored)")
146
+ end
147
+
148
+ def render_write_skipped(result)
149
+ @out.puts("skipped #{result.source_path} -> #{result.target_path} (outside sig root)")
150
+ end
151
+
152
+ def render_write_json(results)
153
+ @out.puts(JSON.pretty_generate({ results: results.map(&:to_h) }))
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../reflection"
4
+ require_relative "../type"
5
+
6
+ module Rigor
7
+ module SigGen
8
+ # ADR-14 follow-up to the dogfood findings: when the
9
+ # inference engine produces a `Type::Nominal` for a class
10
+ # that requires type parameters (`Array`, `Hash`, `Set`,
11
+ # `Range`, `Enumerable`, `Enumerator`, ...) with an empty
12
+ # `type_args` array, the carrier is structurally valid but
13
+ # `erase_to_rbs` renders just `Array` / `Hash` / etc. While
14
+ # RBS itself accepts the bare form, downstream consumers
15
+ # (Steep, IDE plugins, gem-published `sig/` trees) expect
16
+ # the elaborated `Array[untyped]` / `Hash[untyped, untyped]`
17
+ # spelling.
18
+ #
19
+ # This module walks a `Rigor::Type` tree and rebuilds every
20
+ # raw `Nominal[C]` for a generic `C` into `Nominal[C, [Dynamic, ...]]`
21
+ # where the arity comes from
22
+ # `Reflection.class_type_param_names`. The transformation is
23
+ # purely cosmetic — the resulting carrier is structurally
24
+ # distinct from the raw form, but `accepts(other) == accepts(other)`
25
+ # holds because the gradual mode treats `Dynamic[top]`
26
+ # arguments as covering anything.
27
+ #
28
+ # The module is sig-gen-local; the broader question of
29
+ # whether the inference engine itself should always
30
+ # construct generics with explicit type_args is queued as
31
+ # an ADR-14 follow-up.
32
+ module TypeElaborator
33
+ # @param type [Rigor::Type]
34
+ # @param environment [Rigor::Environment]
35
+ # @return [Rigor::Type] same shape with bare generic
36
+ # nominals filled in.
37
+ def self.elaborate(type, environment:)
38
+ arity_cache = {}
39
+ walk(type, environment, arity_cache)
40
+ end
41
+
42
+ def self.walk(type, environment, arity_cache)
43
+ case type
44
+ when Type::Nominal then elaborate_nominal(type, environment, arity_cache)
45
+ when Type::Union then elaborate_union(type, environment, arity_cache)
46
+ when Type::Tuple then elaborate_tuple(type, environment, arity_cache)
47
+ when Type::HashShape then elaborate_hash_shape(type, environment, arity_cache)
48
+ else type
49
+ end
50
+ end
51
+
52
+ def self.elaborate_nominal(type, environment, arity_cache)
53
+ elaborated_args = type.type_args.map { |arg| walk(arg, environment, arity_cache) }
54
+ return Type::Combinator.nominal_of(type.class_name, type_args: elaborated_args) if elaborated_args.any?
55
+
56
+ arity = generic_arity_for(type.class_name, environment, arity_cache)
57
+ return type if arity.zero?
58
+
59
+ filled = Array.new(arity) { Type::Combinator.untyped }
60
+ Type::Combinator.nominal_of(type.class_name, type_args: filled)
61
+ end
62
+
63
+ def self.elaborate_union(type, environment, arity_cache)
64
+ Type::Combinator.union(*type.members.map { |m| walk(m, environment, arity_cache) })
65
+ end
66
+
67
+ def self.elaborate_tuple(type, environment, arity_cache)
68
+ elements = type.elements.map { |e| walk(e, environment, arity_cache) }
69
+ Type::Tuple.new(elements)
70
+ end
71
+
72
+ def self.elaborate_hash_shape(type, _environment, _arity_cache)
73
+ # HashShape's element types are read-only on the carrier;
74
+ # rebuilding them would need going through the per-pair
75
+ # required/optional/read-only machinery. Sig-gen only
76
+ # routes top-level returns through here for now —
77
+ # nested HashShape elaboration ships as a follow-up if
78
+ # the need surfaces.
79
+ type
80
+ end
81
+
82
+ def self.generic_arity_for(class_name, environment, cache)
83
+ return cache[class_name] if cache.key?(class_name)
84
+
85
+ names = Reflection.class_type_param_names(class_name, environment: environment)
86
+ cache[class_name] = names.size
87
+ rescue StandardError
88
+ cache[class_name] = 0
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module SigGen
5
+ # Per-source-file outcome of a `rigor sig-gen --write` run.
6
+ #
7
+ # The writer reports back what it did so the renderer (and
8
+ # the CLI's exit-status logic) can summarise actions and
9
+ # surface user-authored-skip decisions without having to
10
+ # re-parse the produced files.
11
+ #
12
+ # - `source_path` — original `.rb` file.
13
+ # - `target_path` — `.rbs` file the writer was responsible
14
+ # for (`nil` when the source path falls outside the
15
+ # project signature tree, in which case `action` is
16
+ # `:skipped_outside_sig_root`).
17
+ # - `action` — one of `:created` / `:updated` / `:noop` /
18
+ # `:skipped_outside_sig_root`.
19
+ # - `applied` — the {MethodCandidate}s that actually
20
+ # landed on disk.
21
+ # - `skipped` — the {MethodCandidate}s the writer
22
+ # declined (e.g. tighter-return without `--overwrite`).
23
+ # Each entry pairs the candidate with a skip reason
24
+ # keyword (`:user_authored`).
25
+ class WriteResult
26
+ attr_reader :source_path, :target_path, :action, :applied, :skipped
27
+
28
+ def initialize(source_path:, target_path:, action:, applied: [], skipped: [])
29
+ @source_path = source_path
30
+ @target_path = target_path
31
+ @action = action
32
+ @applied = applied.freeze
33
+ @skipped = skipped.freeze
34
+ freeze
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ source: source_path,
40
+ target: target_path&.to_s,
41
+ action: action.to_s,
42
+ applied: applied.map(&:to_h),
43
+ skipped: skipped.map { |c, reason| c.to_h.merge(write_skip_reason: reason.to_s) }
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end