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.
- checksums.yaml +4 -4
- data/README.md +125 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +114 -3
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +2 -2
- data/lib/rigor/environment.rb +35 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +146 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +30 -9
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- 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
|