rigortype 0.2.9 → 0.3.1
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 +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -48,6 +48,30 @@ module Rigor
|
|
|
48
48
|
base.erase_to_rbs
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# True when `removed` is the *empty witness* of `base` — the single value whose removal makes this
|
|
52
|
+
# difference one of the catalogued `non-empty-*` / `non-zero-int` refinements (`""` for String, `0`
|
|
53
|
+
# for Integer, `[]` for Array, `{}` for Hash).
|
|
54
|
+
#
|
|
55
|
+
# This is the shared recogniser for the whole engine: the display side (`canonical_name` below),
|
|
56
|
+
# `ShapeDispatch`'s refinement-aware projections, and `MutationWidening`'s invalidation arm all ask
|
|
57
|
+
# this one question, so the recognised set cannot drift between them.
|
|
58
|
+
def removes_empty_witness?
|
|
59
|
+
return false unless base.is_a?(Nominal)
|
|
60
|
+
|
|
61
|
+
predicate = EMPTY_WITNESS_PREDICATES[base.class_name]
|
|
62
|
+
!!predicate && predicate.call(removed)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
EMPTY_WITNESS_PREDICATES = {
|
|
66
|
+
"String" => ->(removed) { removed.is_a?(Constant) && removed.value == "" },
|
|
67
|
+
"Integer" => lambda { |removed|
|
|
68
|
+
removed.is_a?(Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
69
|
+
},
|
|
70
|
+
"Array" => ->(removed) { removed.is_a?(Tuple) && removed.elements.empty? },
|
|
71
|
+
"Hash" => ->(removed) { removed.is_a?(HashShape) && removed.pairs.empty? }
|
|
72
|
+
}.freeze
|
|
73
|
+
private_constant :EMPTY_WITNESS_PREDICATES
|
|
74
|
+
|
|
51
75
|
def top
|
|
52
76
|
Trinary.no
|
|
53
77
|
end
|
|
@@ -80,47 +104,27 @@ module Rigor
|
|
|
80
104
|
# The recognised set is kept in sync with the imported-built-in catalogue
|
|
81
105
|
# ([`imported-built-in-types.md`](docs/type-specification/imported-built-in-types.md)).
|
|
82
106
|
def canonical_name
|
|
83
|
-
return nil unless
|
|
107
|
+
return nil unless removes_empty_witness?
|
|
84
108
|
|
|
85
|
-
send(CANONICAL_HANDLERS[base.class_name]
|
|
109
|
+
send(CANONICAL_HANDLERS[base.class_name])
|
|
86
110
|
end
|
|
87
111
|
|
|
88
112
|
CANONICAL_HANDLERS = {
|
|
89
113
|
"String" => :string_canonical_name,
|
|
90
114
|
"Integer" => :integer_canonical_name,
|
|
91
|
-
"Array" => :
|
|
92
|
-
"Hash" => :
|
|
115
|
+
"Array" => :array_canonical_name,
|
|
116
|
+
"Hash" => :hash_canonical_name
|
|
93
117
|
}.freeze
|
|
94
118
|
private_constant :CANONICAL_HANDLERS
|
|
95
119
|
|
|
96
|
-
def no_canonical_name
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
99
|
-
|
|
100
120
|
def string_canonical_name
|
|
101
|
-
return nil unless removed.is_a?(Constant) && removed.value == ""
|
|
102
|
-
|
|
103
121
|
"non-empty-string"
|
|
104
122
|
end
|
|
105
123
|
|
|
106
124
|
def integer_canonical_name
|
|
107
|
-
return nil unless removed.is_a?(Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
108
|
-
|
|
109
125
|
"non-zero-int"
|
|
110
126
|
end
|
|
111
127
|
|
|
112
|
-
def array_canonical_name_if_empty
|
|
113
|
-
return nil unless removed.is_a?(Tuple) && removed.elements.empty?
|
|
114
|
-
|
|
115
|
-
array_canonical_name
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def hash_canonical_name_if_empty
|
|
119
|
-
return nil unless removed.is_a?(HashShape) && removed.pairs.empty?
|
|
120
|
-
|
|
121
|
-
hash_canonical_name
|
|
122
|
-
end
|
|
123
|
-
|
|
124
128
|
def array_canonical_name
|
|
125
129
|
elem = base.type_args.first
|
|
126
130
|
return "non-empty-array" if elem.nil?
|
|
@@ -11,9 +11,12 @@ module Rigor
|
|
|
11
11
|
# inhabit the corresponding value types. RBS records correspond to the exact closed subset; Rigor
|
|
12
12
|
# extends that carrier with optional keys, read-only entry views, and an open/closed extra-key policy.
|
|
13
13
|
#
|
|
14
|
-
# Keys are restricted to
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Keys are restricted to value-pinned scalar literals: Symbol, String, Integer, Float, and the
|
|
15
|
+
# `true` / `false` / `nil` singletons. Exact closed symbol-keyed shapes erase to the RBS record
|
|
16
|
+
# syntax `{ a: Integer, ?b: String }`; all other shapes degrade to `Hash[K, V]` or raw `Hash` when
|
|
17
|
+
# no useful bounds are available. Key identity follows Ruby's own `Hash` `eql?` semantics because
|
|
18
|
+
# the pairs are stored in a native Ruby Hash — `1` and `1.0` are DISTINCT keys (`1.eql?(1.0)` is
|
|
19
|
+
# false), while `1.0` and `1.00` are the same key.
|
|
17
20
|
#
|
|
18
21
|
# Equality and hashing are structural over the (key -> Rigor::Type) pair set and policy fields. Hash
|
|
19
22
|
# insertion order is preserved by the underlying storage but does NOT affect equality (matching
|
|
@@ -22,7 +25,7 @@ module Rigor
|
|
|
22
25
|
# See docs/type-specification/rbs-compatible-types.md (records) and
|
|
23
26
|
# docs/type-specification/rigor-extensions.md (hash shape).
|
|
24
27
|
class HashShape
|
|
25
|
-
ALLOWED_KEY_CLASSES = [Symbol, String].freeze
|
|
28
|
+
ALLOWED_KEY_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
26
29
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
27
30
|
POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
|
|
28
31
|
# A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
|
|
@@ -128,7 +131,8 @@ module Rigor
|
|
|
128
131
|
pairs.each_key do |key|
|
|
129
132
|
next if ALLOWED_KEY_CLASSES.any? { |klass| key.is_a?(klass) }
|
|
130
133
|
|
|
131
|
-
raise ArgumentError,
|
|
134
|
+
raise ArgumentError,
|
|
135
|
+
"HashShape keys must be one of #{ALLOWED_KEY_CLASSES.join(', ')}, got #{key.class}"
|
|
132
136
|
end
|
|
133
137
|
end
|
|
134
138
|
|
|
@@ -190,17 +194,28 @@ module Rigor
|
|
|
190
194
|
prefix = []
|
|
191
195
|
prefix << "readonly" if read_only_key?(key)
|
|
192
196
|
rendered_key = optional_key?(key) ? "?#{render_key(key)}" : render_key(key)
|
|
193
|
-
prefix << "#{rendered_key}
|
|
197
|
+
prefix << "#{rendered_key}#{key_separator(key)}"
|
|
194
198
|
"#{prefix.join(' ')} #{value.describe(verbosity)}"
|
|
195
199
|
end
|
|
196
200
|
|
|
197
201
|
# `describe` is a human-facing display contract (it feeds diagnostic messages, never a parser),
|
|
198
|
-
# so it keeps the compact `"a":` form for a quoted key.
|
|
199
|
-
#
|
|
202
|
+
# so it keeps the compact `"a":` form for a quoted key. Non-(Symbol|String) scalar keys render in
|
|
203
|
+
# the natural hashrocket spelling (`1 => 2`, `1.0 => 4`, `nil => 0`) via {#key_separator}; their
|
|
204
|
+
# `inspect` is the canonical Ruby literal, so describe stays deterministic. The RBS-erasure path
|
|
205
|
+
# uses {#erase_key_prefix} instead, which must emit a parseable key.
|
|
200
206
|
def render_key(key)
|
|
201
207
|
case key
|
|
202
208
|
when Symbol then key.to_s
|
|
203
|
-
|
|
209
|
+
else key.inspect
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Symbol / String keys keep the compact colon form (`a: 1`, `"k": 2`); every other scalar key uses
|
|
214
|
+
# the hashrocket (`1 => 2`) — the form the user would have to write in source.
|
|
215
|
+
def key_separator(key)
|
|
216
|
+
case key
|
|
217
|
+
when Symbol, String then ":"
|
|
218
|
+
else " =>"
|
|
204
219
|
end
|
|
205
220
|
end
|
|
206
221
|
|
|
@@ -230,8 +245,17 @@ module Rigor
|
|
|
230
245
|
"Hash[#{key_type.erase_to_rbs}, #{value_type.erase_to_rbs}]"
|
|
231
246
|
end
|
|
232
247
|
|
|
248
|
+
# Conservative per-key-class bound for the `Hash[K, V]` degradation. Symbol / String / Integer /
|
|
249
|
+
# Float keys widen to their class nominal; the `true` / `false` / `nil` singletons keep their
|
|
250
|
+
# literal carrier (the constant IS the class's whole value set, and RBS spells the literal — `nil`
|
|
251
|
+
# reads better than `NilClass`).
|
|
233
252
|
def hash_erasure_key_type
|
|
234
|
-
key_types = pairs.keys.map
|
|
253
|
+
key_types = pairs.keys.map do |key|
|
|
254
|
+
case key
|
|
255
|
+
when true, false, nil then Type::Combinator.constant_of(key)
|
|
256
|
+
else Type::Combinator.nominal_of(key.class)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
235
259
|
Type::Combinator.union(*key_types)
|
|
236
260
|
end
|
|
237
261
|
end
|
data/lib/rigor/version.rb
CHANGED
data/lib/rigortype.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Nothing in an application should ever load Rigor: it is a command-line tool
|
|
4
|
+
# that reads a project from the outside, not a library (ADR-27). This file
|
|
5
|
+
# exists only because `Bundler.require` — the default for every Gemfile entry,
|
|
6
|
+
# and what Rails runs at boot — requires a gem by its *gem* name, `rigortype`,
|
|
7
|
+
# while the library entry point is `rigor.rb`.
|
|
8
|
+
#
|
|
9
|
+
# Without this file that call raises a bare `LoadError` naming `rigortype`, so
|
|
10
|
+
# a `bundle add rigortype` succeeds and the *next* `rails s` dies with no
|
|
11
|
+
# explanation of what went wrong or what to do instead. The file is a guardrail,
|
|
12
|
+
# not an entry point: it warns and defines nothing. `require "rigor"` remains
|
|
13
|
+
# the (analyzer-internal) way in.
|
|
14
|
+
warn(<<~MESSAGE)
|
|
15
|
+
rigortype: `require "rigortype"` does nothing — Rigor is a command-line tool, not a library.
|
|
16
|
+
|
|
17
|
+
This usually means Rigor is in your Gemfile, where `Bundler.require` loads it at boot. Rigor
|
|
18
|
+
analyses your project from the outside and runs on its own Ruby, so a Gemfile entry constrains
|
|
19
|
+
your application's Ruby and dependency resolution without giving you anything. Remove it and
|
|
20
|
+
install Rigor independently: https://github.com/rigortype/rigor/blob/master/docs/install.md
|
|
21
|
+
|
|
22
|
+
To keep the Gemfile entry anyway, mark it `gem "rigortype", require: false` — that silences
|
|
23
|
+
this warning, but the resolution constraints remain.
|
|
24
|
+
MESSAGE
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "channel_index"
|
|
@@ -76,7 +78,7 @@ module Rigor
|
|
|
76
78
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
77
79
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
78
80
|
else
|
|
79
|
-
node.
|
|
81
|
+
node.rigor_each_child { |child| walk_for_channels(child, lexical_path, &) }
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
|
|
@@ -138,7 +140,7 @@ module Rigor
|
|
|
138
140
|
end
|
|
139
141
|
end
|
|
140
142
|
|
|
141
|
-
node.
|
|
143
|
+
node.rigor_each_child do |child|
|
|
142
144
|
names, dynamic = collect_stream_registrations(child, names: names, dynamic: dynamic)
|
|
143
145
|
end
|
|
144
146
|
[names, dynamic]
|
|
@@ -42,6 +42,25 @@ module Rigor
|
|
|
42
42
|
# informational only (deferred: cross-plugin handoff to a JS-side analyzer).
|
|
43
43
|
# - **`broadcast_to` arity isn't checked.** The method takes any record + any data hash; there's no
|
|
44
44
|
# useful arity envelope.
|
|
45
|
+
#
|
|
46
|
+
# ## `#receive(data)` protocol contract (ADR-28)
|
|
47
|
+
#
|
|
48
|
+
# ActionCable's `Connection::Subscriptions#perform_action` dispatches an incoming message to the
|
|
49
|
+
# action named by the payload's `"action"` key, defaulting to `:receive` when that key is absent —
|
|
50
|
+
# i.e. `#receive(data)` is the framework-documented catch-all handler for messages with no explicit
|
|
51
|
+
# action. Either way the single positional argument is always the `ActiveSupport::JSON.decode` of the
|
|
52
|
+
# client-sent payload, which is a JSON object and therefore a `Hash` by convention (the bundled JS
|
|
53
|
+
# client only ever calls `perform(action, data = {})` with an object). This plugin declares a
|
|
54
|
+
# path-scoped protocol contract (`method_name: :receive`, `param_types: [{index: 0, type_name:
|
|
55
|
+
# "Hash"}]`) so `data` types as `Hash` instead of `Dynamic[Top]` inside a channel's `#receive` body.
|
|
56
|
+
#
|
|
57
|
+
# Deliberately narrower than Hanami's `#handle`: custom action methods (`def speak(data)`) also
|
|
58
|
+
# receive the decoded Hash, but their names are project-chosen, not a single fixed Symbol a
|
|
59
|
+
# `ProtocolContract` can name — only `:receive` is a framework-reserved, uniformly-shaped method name.
|
|
60
|
+
# Per ADR-28, the contract is path-scoped, not class-scoped: any `#receive(data)` defined anywhere
|
|
61
|
+
# under the (possibly multi-root) `channel_search_paths` is typed, even on a stray non-channel class
|
|
62
|
+
# that happens to live in that directory — the same accepted-risk shape as `rigor-hanami`'s
|
|
63
|
+
# `#handle`.
|
|
45
64
|
class Actioncable < Rigor::Plugin::Base
|
|
46
65
|
manifest(
|
|
47
66
|
id: "actioncable",
|
|
@@ -53,7 +72,15 @@ module Rigor
|
|
|
53
72
|
kind: :array,
|
|
54
73
|
default: ["ApplicationCable::Channel", "ActionCable::Channel::Base"]
|
|
55
74
|
}
|
|
56
|
-
}
|
|
75
|
+
},
|
|
76
|
+
protocol_contracts: [
|
|
77
|
+
Rigor::Plugin::ProtocolContract.new(
|
|
78
|
+
path_glob: "app/channels/**/*.rb",
|
|
79
|
+
method_name: :receive,
|
|
80
|
+
param_types: [{ index: 0, type_name: "Hash" }]
|
|
81
|
+
# return_type_name: nil — receive's return value is discarded by the framework dispatcher.
|
|
82
|
+
)
|
|
83
|
+
]
|
|
57
84
|
)
|
|
58
85
|
|
|
59
86
|
# `watch:` covers every `.rb` file under the channel search paths so the cache invalidates when
|
|
@@ -70,6 +97,20 @@ module Rigor
|
|
|
70
97
|
def init(_services)
|
|
71
98
|
@channel_search_paths = Array(config.fetch("channel_search_paths")).map(&:to_s)
|
|
72
99
|
@channel_base_classes = Array(config.fetch("channel_base_classes")).map(&:to_s)
|
|
100
|
+
|
|
101
|
+
# ADR-28 WD5 — `channel_search_paths` is user-configurable and may name multiple roots; retarget
|
|
102
|
+
# the manifest's default `app/channels/**/*.rb` glob to the actual configured root(s) so the
|
|
103
|
+
# contract neither goes inert on a project that renames/adds channel directories nor (more
|
|
104
|
+
# importantly, per the project's false-positive discipline) risks matching an unrelated `#receive`
|
|
105
|
+
# outside them. `File::FNM_EXTGLOB` (enabled by `Registry#contracts_for_path`) makes a `{a,b}`
|
|
106
|
+
# brace group a single valid glob for multiple roots.
|
|
107
|
+
@protocol_contracts = manifest.protocol_contracts.map { |c| c.with_path_glob(channel_search_glob) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ADR-28 — override so the per-project `channel_search_paths` config reaches both the engine's
|
|
111
|
+
# parameter-provision tier and this plugin's own path-scoped reasoning.
|
|
112
|
+
def protocol_contracts
|
|
113
|
+
@protocol_contracts || manifest.protocol_contracts
|
|
73
114
|
end
|
|
74
115
|
|
|
75
116
|
# File-level only: the load-error emission. Per-call broadcast validation runs over the
|
|
@@ -91,6 +132,15 @@ module Rigor
|
|
|
91
132
|
|
|
92
133
|
private
|
|
93
134
|
|
|
135
|
+
# Builds the `#receive` contract's `path_glob` from the configured `channel_search_paths`. A single
|
|
136
|
+
# root becomes a plain `**/*.rb` glob; multiple roots become an `FNM_EXTGLOB` brace group so every
|
|
137
|
+
# configured root — and only a configured root — is covered.
|
|
138
|
+
def channel_search_glob
|
|
139
|
+
roots = @channel_search_paths.map { |p| p.chomp("/") }
|
|
140
|
+
base = roots.length == 1 ? roots.first : "{#{roots.join(',')}}"
|
|
141
|
+
"#{base}/**/*.rb"
|
|
142
|
+
end
|
|
143
|
+
|
|
94
144
|
def load_error_diagnostic(path)
|
|
95
145
|
error = producer_error(:channel_index)
|
|
96
146
|
Rigor::Analysis::Diagnostic.new(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -97,7 +99,7 @@ module Rigor
|
|
|
97
99
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
98
100
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
99
101
|
else
|
|
100
|
-
node.
|
|
102
|
+
node.rigor_each_child { |child| walk_for_mailers(child, lexical_path, &) }
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
|
|
@@ -124,7 +126,7 @@ module Rigor
|
|
|
124
126
|
return [] if body.nil?
|
|
125
127
|
|
|
126
128
|
names = []
|
|
127
|
-
body.
|
|
129
|
+
body.rigor_each_child do |node|
|
|
128
130
|
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :include
|
|
129
131
|
|
|
130
132
|
(node.arguments&.arguments || []).each do |arg|
|
|
@@ -158,7 +160,7 @@ module Rigor
|
|
|
158
160
|
inner = local_name ? lexical_path + [local_name.delete_prefix("::")] : lexical_path
|
|
159
161
|
collect_module_actions(node.body, inner, accumulator) if node.body
|
|
160
162
|
else
|
|
161
|
-
node.
|
|
163
|
+
node.rigor_each_child { |child| collect_module_actions(child, lexical_path, accumulator) }
|
|
162
164
|
end
|
|
163
165
|
end
|
|
164
166
|
|
|
@@ -231,7 +233,7 @@ module Rigor
|
|
|
231
233
|
private_names = []
|
|
232
234
|
callback_names = []
|
|
233
235
|
|
|
234
|
-
body.
|
|
236
|
+
body.rigor_each_child do |node|
|
|
235
237
|
next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
236
238
|
|
|
237
239
|
args = (node.arguments&.arguments || []).filter_map do |arg|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "controller_index"
|
|
@@ -89,7 +91,7 @@ module Rigor
|
|
|
89
91
|
|
|
90
92
|
walk_declarations(node.body, namespace: inner_namespace, &)
|
|
91
93
|
else
|
|
92
|
-
node.
|
|
94
|
+
node.rigor_each_child do |child|
|
|
93
95
|
walk_declarations(child, namespace: namespace, &)
|
|
94
96
|
end
|
|
95
97
|
end
|
|
@@ -142,7 +144,7 @@ module Rigor
|
|
|
142
144
|
return accumulator unless node.is_a?(Prism::Node)
|
|
143
145
|
|
|
144
146
|
accumulator << node.name if node.is_a?(Prism::DefNode) && node.receiver.nil?
|
|
145
|
-
node.
|
|
147
|
+
node.rigor_each_child { |child| collect_def_names(child, accumulator) }
|
|
146
148
|
accumulator
|
|
147
149
|
end
|
|
148
150
|
|
|
@@ -160,7 +162,7 @@ module Rigor
|
|
|
160
162
|
end
|
|
161
163
|
end
|
|
162
164
|
|
|
163
|
-
node.
|
|
165
|
+
node.rigor_each_child { |child| collect_include_targets(child, accumulator) }
|
|
164
166
|
accumulator
|
|
165
167
|
end
|
|
166
168
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "job_index"
|
|
@@ -66,7 +68,7 @@ module Rigor
|
|
|
66
68
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
67
69
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
68
70
|
else
|
|
69
|
-
node.
|
|
71
|
+
node.rigor_each_child { |child| walk_for_jobs(child, lexical_path, &) }
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
|
|
@@ -118,7 +120,7 @@ module Rigor
|
|
|
118
120
|
def lookup_perform_def(body)
|
|
119
121
|
return nil if body.nil?
|
|
120
122
|
|
|
121
|
-
body.
|
|
123
|
+
body.rigor_each_child do |node|
|
|
122
124
|
next unless node.is_a?(Prism::DefNode) && node.name == :perform
|
|
123
125
|
next if node.receiver.is_a?(Prism::SelfNode)
|
|
124
126
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -50,7 +52,7 @@ module Rigor
|
|
|
50
52
|
return if node.nil?
|
|
51
53
|
|
|
52
54
|
yield node
|
|
53
|
-
node.
|
|
55
|
+
node.rigor_each_child { |child| walk(child, &) }
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def visit_call(node)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -140,7 +142,7 @@ module Rigor
|
|
|
140
142
|
when Prism::ModuleNode
|
|
141
143
|
visit_module(node, lexical_path, &)
|
|
142
144
|
else
|
|
143
|
-
node.
|
|
145
|
+
node.rigor_each_child { |child| walk_for_classes(child, lexical_path, &) }
|
|
144
146
|
end
|
|
145
147
|
end
|
|
146
148
|
|
|
@@ -256,7 +258,7 @@ module Rigor
|
|
|
256
258
|
def lookup_table_name_override(body)
|
|
257
259
|
return nil if body.nil?
|
|
258
260
|
|
|
259
|
-
body.
|
|
261
|
+
body.rigor_each_child do |node|
|
|
260
262
|
next unless node.is_a?(Prism::CallNode) && node.name == :table_name=
|
|
261
263
|
next unless node.receiver.is_a?(Prism::SelfNode)
|
|
262
264
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -66,7 +68,7 @@ module Rigor
|
|
|
66
68
|
|
|
67
69
|
yield node if node.is_a?(Prism::CallNode) && node.name == :create_table && node.receiver.nil?
|
|
68
70
|
|
|
69
|
-
node.
|
|
71
|
+
node.rigor_each_child { |child| collect_create_table_calls(child, &) }
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def parse_create_table(call_node)
|
|
@@ -118,7 +120,7 @@ module Rigor
|
|
|
118
120
|
return
|
|
119
121
|
end
|
|
120
122
|
|
|
121
|
-
node.
|
|
123
|
+
node.rigor_each_child { |child| collect_column_calls(child, &) }
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
def parse_column(call_node)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -32,7 +34,7 @@ module Rigor
|
|
|
32
34
|
return if node.nil?
|
|
33
35
|
|
|
34
36
|
yield node
|
|
35
|
-
node.
|
|
37
|
+
node.rigor_each_child { |child| walk(child, &) }
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def visit_call(node)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -77,7 +79,7 @@ module Rigor
|
|
|
77
79
|
when Prism::ModuleNode
|
|
78
80
|
visit_module(node, lexical_path, &)
|
|
79
81
|
else
|
|
80
|
-
node.
|
|
82
|
+
node.rigor_each_child { |child| walk(child, lexical_path, &) }
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
|
|
@@ -105,7 +107,7 @@ module Rigor
|
|
|
105
107
|
return [] if body.nil?
|
|
106
108
|
|
|
107
109
|
rows = []
|
|
108
|
-
body.
|
|
110
|
+
body.rigor_each_child do |node|
|
|
109
111
|
next unless node.is_a?(Prism::CallNode)
|
|
110
112
|
|
|
111
113
|
kind = ATTACHMENT_METHODS[node.name]
|
|
@@ -63,12 +63,28 @@ module Rigor
|
|
|
63
63
|
produces: [:dry_type_aliases]
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
66
|
+
# Cached alias-table producer (ADR-60 WD3 record-and-validate). The scan Prism-parses every `.rb` file
|
|
67
|
+
# under the project's `paths:` tree to find `include Dry.Types()` declarations — an expensive pass that
|
|
68
|
+
# ran on EVERY invocation (cold and warm) because `#prepare` called {AliasScanner.scan} directly, with
|
|
69
|
+
# no cache. It now rides `cache_for`: a warm run re-globs + re-digests the watched tree (cheap SHA over
|
|
70
|
+
# file bytes, no AST build) and reuses the cached table instead of re-parsing. This is the dominant win
|
|
71
|
+
# even for a project that ships no dry-types module — the parse still has to run to prove that, and now
|
|
72
|
+
# the empty result is cached too.
|
|
73
|
+
#
|
|
74
|
+
# `watch:` covers exactly the files the scan reads: one glob per project `paths:` entry, co-extensive
|
|
75
|
+
# with {#scannable_paths}. A {Cache::Descriptor::GlobEntry} digests every file matching its glob, so a
|
|
76
|
+
# content edit, a file addition, and a file removal anywhere under those paths all move the digest and
|
|
77
|
+
# invalidate the entry. The scan reads no file individually through the `IoBoundary`, so the evaluated
|
|
78
|
+
# `watch:` globs are the entire dependency descriptor — and they fully cover the scan's input.
|
|
79
|
+
producer :dry_type_aliases, watch: -> { alias_watch_globs } do |_params|
|
|
80
|
+
AliasScanner.scan(paths: scannable_paths)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Builds the (cached) alias table and publishes it via the ADR-9 fact store. `producer_value` runs the
|
|
84
|
+
# producer through `cache_for` and memoises the result — including a rescued nil — for the run.
|
|
69
85
|
def prepare(services)
|
|
70
|
-
aliases =
|
|
71
|
-
return if aliases.empty?
|
|
86
|
+
aliases = producer_value(:dry_type_aliases)
|
|
87
|
+
return if aliases.nil? || aliases.empty?
|
|
72
88
|
|
|
73
89
|
services.fact_store.publish(
|
|
74
90
|
plugin_id: manifest.id,
|
|
@@ -86,7 +102,7 @@ module Rigor
|
|
|
86
102
|
# Resolves the project's `paths:` to a flat list of `.rb` files the scanner walks. Mirrors
|
|
87
103
|
# `Analysis::Runner`'s `expand_paths` floor; we don't need the runner's full exclude/sort surface
|
|
88
104
|
# because the alias table is a union — any duplicate scan is a no-op.
|
|
89
|
-
def scannable_paths
|
|
105
|
+
def scannable_paths
|
|
90
106
|
@scannable_paths ||= services.configuration.paths.flat_map do |entry|
|
|
91
107
|
if File.directory?(entry)
|
|
92
108
|
Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
|
|
@@ -97,6 +113,22 @@ module Rigor
|
|
|
97
113
|
end
|
|
98
114
|
end.uniq.freeze
|
|
99
115
|
end
|
|
116
|
+
|
|
117
|
+
# The `watch:` glob tuples covering the scan's entire input — one `[root, pattern]` per project `paths:`
|
|
118
|
+
# entry. A directory entry watches its whole `**/*.rb` subtree; a single-file `.rb` entry watches
|
|
119
|
+
# exactly that file (`[dir, basename]`). Roots are expanded to absolute paths by
|
|
120
|
+
# `Plugin::Base#watch_glob_entries`, so a relative CLI path (`app/models`) re-validates against the same
|
|
121
|
+
# tree from the project root on the next run. Co-extensive with {#scannable_paths}: every `.rb` the scan
|
|
122
|
+
# would read is watched, so any edit / addition / removal under those paths invalidates the cache.
|
|
123
|
+
def alias_watch_globs
|
|
124
|
+
services.configuration.paths.filter_map do |entry|
|
|
125
|
+
if File.directory?(entry)
|
|
126
|
+
[entry, "**/*.rb"]
|
|
127
|
+
elsif File.file?(entry) && entry.end_with?(".rb")
|
|
128
|
+
[File.dirname(entry), File.basename(entry)]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
100
132
|
end
|
|
101
133
|
|
|
102
134
|
Rigor::Plugin.register(DryTypes)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -86,7 +88,7 @@ module Rigor
|
|
|
86
88
|
visit_factory(node, &)
|
|
87
89
|
return
|
|
88
90
|
end
|
|
89
|
-
node.
|
|
91
|
+
node.rigor_each_child { |child| walk_for_factories(child, &) }
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def factory_call?(node)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -65,7 +67,7 @@ module Rigor
|
|
|
65
67
|
case node
|
|
66
68
|
when Prism::DefNode then defs << node
|
|
67
69
|
when Prism::ClassNode, Prism::ModuleNode then nil # nested scopes own their own defs
|
|
68
|
-
else node.
|
|
70
|
+
else node.rigor_each_child { |child| collect_direct_defs(child, defs) }
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -122,6 +122,16 @@ module Rigor
|
|
|
122
122
|
carried_type(receiver_type, call_node.name)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# ADR-88 WD1 — the `dynamic_return` contribution is a PURE STRUCTURAL projection of the receiver's own
|
|
126
|
+
# carrier type args (`Result[T, E]#unwrap → T`), computed per call from the receiver type the engine
|
|
127
|
+
# already resolved; it reads no cross-file catalog. (The `Enum` nested-class emission rides the ADR-16
|
|
128
|
+
# macro substrate / synthetic-method scanner, a separate pre-pass, not this `dynamic_return`.) A stable
|
|
129
|
+
# sentinel declares "no cross-file fact surface", keeping the plugin incremental-capable; the
|
|
130
|
+
# `--verify-incremental` gate backstops the claim.
|
|
131
|
+
def incremental_state_fingerprint
|
|
132
|
+
"structural-carriers"
|
|
133
|
+
end
|
|
134
|
+
|
|
125
135
|
private
|
|
126
136
|
|
|
127
137
|
# @return [Rigor::Type, nil] the receiver's inferred type, or nil when the engine raises on a synthetic
|