rigortype 0.2.9 → 0.3.0
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/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- 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 +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- 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 +68 -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 +343 -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_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- 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 +212 -27
- 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 +163 -23
- 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 +1 -32
- 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 +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- 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 +11 -0
- 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/completion_provider.rb +2 -1
- 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/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- 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 +24 -2
- 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-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/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- 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 +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
data/lib/rigor/scope.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "analysis/dependency_recorder"
|
|
|
8
8
|
require_relative "inference/expression_typer"
|
|
9
9
|
require_relative "inference/flow_tracer"
|
|
10
10
|
require_relative "inference/statement_evaluator"
|
|
11
|
+
require_relative "inference/def_node_resolver"
|
|
11
12
|
|
|
12
13
|
module Rigor
|
|
13
14
|
# Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
|
|
@@ -22,7 +23,8 @@ module Rigor
|
|
|
22
23
|
:indexed_narrowings, :method_chain_narrowings,
|
|
23
24
|
:declaration_sourced,
|
|
24
25
|
:source_path, :discovery, :struct_fold_safe_locals,
|
|
25
|
-
:dynamic_origins, :local_origins, :ivar_origins
|
|
26
|
+
:dynamic_origins, :local_origins, :ivar_origins,
|
|
27
|
+
:void_origins
|
|
26
28
|
|
|
27
29
|
# ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
|
|
28
30
|
# reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
|
|
@@ -41,6 +43,7 @@ module Rigor
|
|
|
41
43
|
def discovered_def_nodes = @discovery.discovered_def_nodes
|
|
42
44
|
def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes
|
|
43
45
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
46
|
+
def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources
|
|
44
47
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
45
48
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
46
49
|
def discovered_includes = @discovery.discovered_includes
|
|
@@ -52,6 +55,9 @@ module Rigor
|
|
|
52
55
|
# call-site argument types (precision-additive; an RBS-declared parameter always wins). Empty unless a
|
|
53
56
|
# collection pass seeded it.
|
|
54
57
|
def param_inferred_types = @discovery.param_inferred_types
|
|
58
|
+
# ADR-84 WD2 — the per-run identity token the user-method return memo buckets on (nil outside runner-seeded
|
|
59
|
+
# scopes; the memo then falls back to the per-file `discovered_def_nodes` identity).
|
|
60
|
+
def run_generation = @discovery.run_generation
|
|
55
61
|
|
|
56
62
|
# Narrowing key for an indexed read `receiver[key]` where both the receiver and the key are stable enough to
|
|
57
63
|
# address. The value of the map at this key is the narrowed type the next read at the same address MUST
|
|
@@ -117,6 +123,16 @@ module Rigor
|
|
|
117
123
|
self
|
|
118
124
|
end
|
|
119
125
|
|
|
126
|
+
# ADR-100 WD3 — records that the value introduced at `node` (a call node) is a `top` recovered from an
|
|
127
|
+
# author-declared `-> void` return, keyed by the origin site (`origin`, an {Inference::VoidOrigin}). The
|
|
128
|
+
# value-context check rule `static.value-use.void` consumes this table. Mirrors {#record_dynamic_origin}
|
|
129
|
+
# exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
|
|
130
|
+
# through `#join` / `#rebuild`), excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
|
|
131
|
+
def record_void_origin(node, origin)
|
|
132
|
+
@void_origins[node] = origin
|
|
133
|
+
self
|
|
134
|
+
end
|
|
135
|
+
|
|
120
136
|
def initialize(
|
|
121
137
|
environment:, locals:,
|
|
122
138
|
fact_store: Analysis::FactStore.empty,
|
|
@@ -132,7 +148,8 @@ module Rigor
|
|
|
132
148
|
struct_fold_safe_locals: EMPTY_FOLD_SAFE,
|
|
133
149
|
dynamic_origins: {}.compare_by_identity,
|
|
134
150
|
local_origins: EMPTY_ORIGINS,
|
|
135
|
-
ivar_origins: EMPTY_ORIGINS
|
|
151
|
+
ivar_origins: EMPTY_ORIGINS,
|
|
152
|
+
void_origins: {}.compare_by_identity
|
|
136
153
|
)
|
|
137
154
|
@environment = environment
|
|
138
155
|
@locals = locals
|
|
@@ -150,6 +167,7 @@ module Rigor
|
|
|
150
167
|
@dynamic_origins = dynamic_origins
|
|
151
168
|
@local_origins = local_origins
|
|
152
169
|
@ivar_origins = ivar_origins
|
|
170
|
+
@void_origins = void_origins
|
|
153
171
|
freeze
|
|
154
172
|
end
|
|
155
173
|
|
|
@@ -294,6 +312,39 @@ module Rigor
|
|
|
294
312
|
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
295
313
|
end
|
|
296
314
|
|
|
315
|
+
# ADR-67 WD6b — stamp the "inferred, not declared" provenance mark on a parameter local seeded from the
|
|
316
|
+
# call-site parameter-inference table ({Inference::ParameterInferenceCollector}). Rides the ADR-58 WD1
|
|
317
|
+
# declaration-sourced side-mark machinery under a distinct `:inferred_param` kind (never a carrier field,
|
|
318
|
+
# so the displayed type is unchanged) so the negative in-body rules can decline on a receiver / argument
|
|
319
|
+
# whose type is an open-call-site *lower bound* — firing against a lower bound is a false positive by
|
|
320
|
+
# construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
|
|
321
|
+
# distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy `:local` mark, which a
|
|
322
|
+
# later un-guarding slice (WD6b) needs. `with_local` drops it on any flow-live rewrite of the local
|
|
323
|
+
# (`drop_local_declaration_marks`), so only the pristine parameter binding carries it.
|
|
324
|
+
def with_inferred_param_mark(name)
|
|
325
|
+
rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name))
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# ADR-67 WD6b — explicitly clear the inferred-parameter taint on `name`. The mark is deliberately STICKY:
|
|
329
|
+
# `with_local` (used by both narrowing and reassignment) does NOT drop it, so it survives the narrowing /
|
|
330
|
+
# join transitions between a lower-bound value's definition and its use (`v = param[i]-1; if 0<=v and v<n`
|
|
331
|
+
# — the `and` narrows `v` between the two comparisons). It is cleared only at a genuine source-level local
|
|
332
|
+
# write whose RHS does not derive from an inferred parameter ({StatementEvaluator#eval_local_write}), so a
|
|
333
|
+
# local rebound to an independent value stops being treated as a lower bound. Over-retention (a mark that
|
|
334
|
+
# outlives a rebind the write-path did not catch) only ever suppresses a diagnostic — the FP-safe
|
|
335
|
+
# direction — so stickiness is the conservative choice. Zero-alloc when no mark is present.
|
|
336
|
+
def without_inferred_param_mark(name)
|
|
337
|
+
dropped = drop_declaration_sourced_for(:inferred_param, name)
|
|
338
|
+
dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# ADR-67 WD6b — true when `name`'s local binding is a pristine inferred parameter (the call-site union
|
|
342
|
+
# seeded at method entry, untouched by a flow-live write). The guard predicate the negative in-body rules
|
|
343
|
+
# consult.
|
|
344
|
+
def inferred_param?(name)
|
|
345
|
+
@declaration_sourced.include?([:inferred_param, name.to_sym])
|
|
346
|
+
end
|
|
347
|
+
|
|
297
348
|
def with_cvar(name, type)
|
|
298
349
|
rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
|
|
299
350
|
end
|
|
@@ -366,9 +417,12 @@ module Rigor
|
|
|
366
417
|
# inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
|
|
367
418
|
def user_def_for(class_name, method_name)
|
|
368
419
|
table = @discovery.discovered_def_nodes[class_name.to_s]
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
node
|
|
420
|
+
# ADR-85 WD3 — the value is either a live `Prism::DefNode` (cold / re-walked file) or a `DefHandle`
|
|
421
|
+
# (unchanged file, bundle-rebuilt index). Dependency recording keys on the table's PRESENCE (both are
|
|
422
|
+
# truthy), so it is sound regardless of resolution; only the returned node is resolved lazily.
|
|
423
|
+
entry = table && table[method_name.to_sym]
|
|
424
|
+
record_cross_file_method(class_name, method_name, entry) if Analysis::DependencyRecorder.active?
|
|
425
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
372
426
|
end
|
|
373
427
|
|
|
374
428
|
# Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
|
|
@@ -379,25 +433,28 @@ module Rigor
|
|
|
379
433
|
# the same cross-file dependency edge as the instance path (ADR-46).
|
|
380
434
|
def singleton_def_for(class_name, method_name)
|
|
381
435
|
table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
|
|
382
|
-
|
|
383
|
-
record_cross_file_method(class_name, method_name,
|
|
384
|
-
|
|
436
|
+
entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3)
|
|
437
|
+
record_cross_file_method(class_name, method_name, entry, singleton: true) if Analysis::DependencyRecorder.active?
|
|
438
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
385
439
|
end
|
|
386
440
|
|
|
387
441
|
# ADR-46 slice 1 — note the cross-file dependency this resolution creates: the file defining
|
|
388
442
|
# `class_name#method_name` (the consumer's analysis reads its body via `infer_user_method_return`), or, when
|
|
389
443
|
# unresolved, a negative edge so a later definition re-checks the consumer. Gated on the recorder being
|
|
390
|
-
# active — no-op on a normal run.
|
|
391
|
-
|
|
444
|
+
# active — no-op on a normal run. `singleton:` selects the singleton-side source table + a `"Class.method"`
|
|
445
|
+
# symbol key (vs the instance `"Class#method"`), so a class/singleton-method body edit produces a changed
|
|
446
|
+
# symbol pair and scopes to the method's call sites the same way an instance-method edit does — the source
|
|
447
|
+
# site is read from `discovered_singleton_def_sources`, the mirror the ScopeIndexer now records (ADR-46
|
|
448
|
+
# slice 4 singleton extension). Both keys share the format `Runner#symbol_fingerprints` emits.
|
|
449
|
+
def record_cross_file_method(class_name, method_name, node, singleton: false)
|
|
450
|
+
symbol = "#{class_name}#{singleton ? '.' : '#'}#{method_name}"
|
|
392
451
|
if node
|
|
393
452
|
# ADR-46 slice 4 — pass the symbol so the recorder tracks this as a method-call (symbol-granularity) edge
|
|
394
453
|
# rather than a file-level edge.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
"#{class_name}##{method_name}"
|
|
398
|
-
)
|
|
454
|
+
source_table = singleton ? @discovery.discovered_singleton_def_sources : @discovery.discovered_def_sources
|
|
455
|
+
Analysis::DependencyRecorder.read_site(source_table.dig(class_name.to_s, method_name.to_sym), symbol)
|
|
399
456
|
else
|
|
400
|
-
Analysis::DependencyRecorder.read_missing(:method,
|
|
457
|
+
Analysis::DependencyRecorder.read_missing(:method, symbol)
|
|
401
458
|
end
|
|
402
459
|
end
|
|
403
460
|
private :record_cross_file_method
|
|
@@ -408,9 +465,9 @@ module Rigor
|
|
|
408
465
|
# implementation detail and go through this accessor.
|
|
409
466
|
def top_level_def_for(method_name)
|
|
410
467
|
table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
|
|
411
|
-
|
|
412
|
-
record_cross_file_toplevel(method_name,
|
|
413
|
-
|
|
468
|
+
entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3)
|
|
469
|
+
record_cross_file_toplevel(method_name, entry) if Analysis::DependencyRecorder.active?
|
|
470
|
+
Inference::DefNodeResolver.resolve(entry)
|
|
414
471
|
end
|
|
415
472
|
|
|
416
473
|
# ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
|
|
@@ -440,9 +497,16 @@ module Rigor
|
|
|
440
497
|
# can point at `pre_eval:` (ADR-17) instead of reading as a bare unresolved call.
|
|
441
498
|
def user_def_site_for(class_name, method_name)
|
|
442
499
|
table = @discovery.discovered_def_sources[class_name.to_s]
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
500
|
+
site = table && table[method_name.to_sym]
|
|
501
|
+
# ADR-88 WD3 — record the SAME instance-side cross-file method edge {#user_def_for} records at :378, so a
|
|
502
|
+
# move / body-edit of `class_name#method_name`'s definition re-checks the consumer that named the
|
|
503
|
+
# defining file. `CheckRules#undefined_method_diagnostic` reads this to set `project_definition_site`
|
|
504
|
+
# (`"path:line"`) on a `call.undefined-method` for a project monkey-patch; without the edge, a line-shift
|
|
505
|
+
# in the defining file left the cached diagnostic pointing at a stale line (the ADR-46 symbol-granularity
|
|
506
|
+
# closure never re-checked the caller). Recording keys on the source-entry PRESENCE (truthy `site`),
|
|
507
|
+
# sound whether or not the caller also went through {#user_def_for}.
|
|
508
|
+
record_cross_file_method(class_name, method_name, site) if Analysis::DependencyRecorder.active?
|
|
509
|
+
site
|
|
446
510
|
end
|
|
447
511
|
|
|
448
512
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
|
|
@@ -592,6 +656,14 @@ module Rigor
|
|
|
592
656
|
Inference::ExpressionTyper.new(scope: self, tracer: tracer).type_of(node)
|
|
593
657
|
end
|
|
594
658
|
|
|
659
|
+
# ADR-89 WD2 — the inferred return type of `def_node` called with `receiver` / `arg_types`, computed
|
|
660
|
+
# against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
|
|
661
|
+
# session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
|
|
662
|
+
# return is unchanged before skipping its symbol dependents.
|
|
663
|
+
def user_method_return(def_node, receiver, arg_types)
|
|
664
|
+
Inference::ExpressionTyper.new(scope: self).return_type_for(def_node, receiver, arg_types)
|
|
665
|
+
end
|
|
666
|
+
|
|
595
667
|
# Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
|
|
596
668
|
# `scope'` is the scope observable after the node has run. The receiver scope is never mutated. See
|
|
597
669
|
# {Rigor::Inference::StatementEvaluator} for the catalogue of nodes that thread scope; everything else defers
|
|
@@ -651,7 +723,8 @@ module Rigor
|
|
|
651
723
|
struct_fold_safe_locals: @struct_fold_safe_locals,
|
|
652
724
|
dynamic_origins: @dynamic_origins,
|
|
653
725
|
local_origins: @local_origins,
|
|
654
|
-
ivar_origins: @ivar_origins
|
|
726
|
+
ivar_origins: @ivar_origins,
|
|
727
|
+
void_origins: @void_origins
|
|
655
728
|
)
|
|
656
729
|
self.class.new(
|
|
657
730
|
environment: environment, locals: locals,
|
|
@@ -665,7 +738,8 @@ module Rigor
|
|
|
665
738
|
struct_fold_safe_locals: struct_fold_safe_locals,
|
|
666
739
|
dynamic_origins: dynamic_origins,
|
|
667
740
|
local_origins: local_origins,
|
|
668
|
-
ivar_origins: ivar_origins
|
|
741
|
+
ivar_origins: ivar_origins,
|
|
742
|
+
void_origins: void_origins
|
|
669
743
|
)
|
|
670
744
|
end
|
|
671
745
|
|
|
@@ -702,7 +776,8 @@ module Rigor
|
|
|
702
776
|
source_path: source_path,
|
|
703
777
|
dynamic_origins: @dynamic_origins,
|
|
704
778
|
local_origins: join_origins(@local_origins, other.local_origins),
|
|
705
|
-
ivar_origins: join_origins(@ivar_origins, other.ivar_origins)
|
|
779
|
+
ivar_origins: join_origins(@ivar_origins, other.ivar_origins),
|
|
780
|
+
void_origins: @void_origins
|
|
706
781
|
)
|
|
707
782
|
end
|
|
708
783
|
|
|
@@ -724,11 +799,30 @@ module Rigor
|
|
|
724
799
|
origins.key?(key) ? origins.reject { |k, _| k == key }.freeze : origins
|
|
725
800
|
end
|
|
726
801
|
|
|
802
|
+
# Two kinds of mark share this Set with opposite join semantics:
|
|
803
|
+
#
|
|
804
|
+
# - ADR-58 WD1 declaration-sourced optionality (`:ivar` / `:local`) joins by **intersection** — a nil is
|
|
805
|
+
# assumed a cross-method invariant only when BOTH branches agree its optionality is declaration-sourced.
|
|
806
|
+
# - ADR-67 WD6b inferred-parameter taint (`:inferred_param`) joins by **union** — a local is a lower-bound
|
|
807
|
+
# value on the branch where it derives from an inferred parameter, so if EITHER branch taints it, a
|
|
808
|
+
# downstream use could observe the lower-bound value and firing on it is an FP (`x = param else x = 5;
|
|
809
|
+
# x.foo`). Intersecting would lose the taint at the merge and re-surface the false positive.
|
|
727
810
|
def join_declaration_sourced(other)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
811
|
+
mine = @declaration_sourced
|
|
812
|
+
theirs = other.declaration_sourced
|
|
813
|
+
return mine if mine.equal?(theirs)
|
|
814
|
+
|
|
815
|
+
inferred = Set.new
|
|
816
|
+
mine.each { |ref| inferred << ref if ref[0] == :inferred_param }
|
|
817
|
+
theirs.each { |ref| inferred << ref if ref[0] == :inferred_param }
|
|
818
|
+
intersected =
|
|
819
|
+
if mine.empty? || theirs.empty?
|
|
820
|
+
EMPTY_DECLARATION_SOURCED
|
|
821
|
+
else
|
|
822
|
+
mine.select { |ref| ref[0] != :inferred_param && theirs.include?(ref) }
|
|
823
|
+
end
|
|
824
|
+
merged = inferred.merge(intersected)
|
|
825
|
+
merged.empty? ? EMPTY_DECLARATION_SOURCED : merged.freeze
|
|
732
826
|
end
|
|
733
827
|
|
|
734
828
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
@@ -25,7 +25,11 @@ module Rigor
|
|
|
25
25
|
SKIP_DIAGNOSTIC_IDS = {
|
|
26
26
|
complex_shape: "sig.skipped.complex-shape",
|
|
27
27
|
user_authored: "sig.skipped.user-authored",
|
|
28
|
-
untyped_return: "sig.skipped.untyped-return"
|
|
28
|
+
untyped_return: "sig.skipped.untyped-return",
|
|
29
|
+
# The generator rendered a line `rbs` itself rejects. Skipping the method keeps sig-gen useful on a
|
|
30
|
+
# project with one pathological def, where failing the command outright would deny the user every other
|
|
31
|
+
# signature; the count is reported so the defect is not silent. See {SigGen::RbsValidity}.
|
|
32
|
+
unrenderable_rbs: "sig.skipped.unrenderable-rbs"
|
|
29
33
|
}.freeze
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -8,9 +8,11 @@ require_relative "../scope"
|
|
|
8
8
|
require_relative "../reflection"
|
|
9
9
|
require_relative "../type"
|
|
10
10
|
require_relative "../source/literals"
|
|
11
|
+
require_relative "../source/node_children"
|
|
11
12
|
require_relative "../inference/def_return_typer"
|
|
12
13
|
require_relative "../inference/scope_indexer"
|
|
13
14
|
require_relative "../inference/rbs_type_translator"
|
|
15
|
+
require_relative "rbs_validity"
|
|
14
16
|
|
|
15
17
|
module Rigor
|
|
16
18
|
module SigGen
|
|
@@ -33,6 +35,13 @@ module Rigor
|
|
|
33
35
|
# strictly accepts the inferred one (acceptance check under the engine's current `:gradual` mode; ADR-14
|
|
34
36
|
# reserves the eventual `:strict` mode).
|
|
35
37
|
class Generator # rubocop:disable Metrics/ClassLength
|
|
38
|
+
# Methods the generator rendered into RBS that `rbs` itself rejects. Populated by {#build_candidate}; each
|
|
39
|
+
# one is a Rigor rendering DEFECT, not a property of the user's code, so the CLI reports them as such.
|
|
40
|
+
UnrenderableMethod = Data.define(:path, :class_name, :method_name, :rbs, :error)
|
|
41
|
+
|
|
42
|
+
# @return [Array<UnrenderableMethod>] empty on a healthy run; read after {#run}.
|
|
43
|
+
attr_reader :unrenderable
|
|
44
|
+
|
|
36
45
|
# @param configuration [Rigor::Configuration]
|
|
37
46
|
# @param paths [Array<String>] files / directories to scan.
|
|
38
47
|
# @param observations [Hash{[String, Symbol] => Array<Array<Rigor::Type>>}]
|
|
@@ -52,6 +61,8 @@ module Rigor
|
|
|
52
61
|
@module_function_methods = Set.new
|
|
53
62
|
@class_shells = Set.new
|
|
54
63
|
@class_superclasses = {}
|
|
64
|
+
# Whole-run, NOT per-file: a rendering defect is reported once at the end of the run.
|
|
65
|
+
@unrenderable = []
|
|
55
66
|
end
|
|
56
67
|
|
|
57
68
|
# Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
|
|
@@ -159,7 +170,7 @@ module Rigor
|
|
|
159
170
|
return
|
|
160
171
|
end
|
|
161
172
|
|
|
162
|
-
node.
|
|
173
|
+
node.rigor_each_child do |child|
|
|
163
174
|
walk_defs(child, prefix, in_singleton_class, module_function_active, out)
|
|
164
175
|
end
|
|
165
176
|
end
|
|
@@ -264,12 +275,33 @@ module Rigor
|
|
|
264
275
|
# Wraps `MethodCandidate.new` so every candidate carries the per-file `@namespace_kinds` map AND the
|
|
265
276
|
# `@class_shells` set — the Writer's nested-syntax emission consults both to pick `module` vs `class` for
|
|
266
277
|
# each segment and to emit empty `Const = Data.define(...)` declarations.
|
|
267
|
-
|
|
278
|
+
#
|
|
279
|
+
# It is also where every rendered line is PARSED before it can leave the generator (see
|
|
280
|
+
# {SigGen::RbsValidity}). This is the one construction point all candidates pass through, so guarding it
|
|
281
|
+
# covers every mode — `--print`, `--diff`, `--write`, and the MCP surface — rather than each emit site
|
|
282
|
+
# separately. A line `rbs` rejects demotes the candidate to `:skipped` instead of being emitted: a
|
|
283
|
+
# signature we cannot parse is worse than no signature, because it takes the whole FILE down with it
|
|
284
|
+
# (the consumer quarantines it, so every other type in that file vanishes too).
|
|
285
|
+
def build_candidate(**fields)
|
|
286
|
+
fields = demote_unrenderable(fields)
|
|
268
287
|
MethodCandidate.new(
|
|
269
288
|
namespace_kinds: @namespace_kinds,
|
|
270
289
|
class_shells: @class_shells.to_a,
|
|
271
290
|
class_superclasses: @class_superclasses,
|
|
272
|
-
**
|
|
291
|
+
**fields
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def demote_unrenderable(fields)
|
|
296
|
+
error = RbsValidity.method_line_error(fields[:rbs])
|
|
297
|
+
return fields if error.nil?
|
|
298
|
+
|
|
299
|
+
@unrenderable << UnrenderableMethod.new(
|
|
300
|
+
path: fields[:path], class_name: fields[:class_name],
|
|
301
|
+
method_name: fields[:method_name], rbs: fields[:rbs], error: error
|
|
302
|
+
)
|
|
303
|
+
fields.merge(
|
|
304
|
+
classification: Classification::SKIPPED, skip_reason: :unrenderable_rbs, rbs: nil
|
|
273
305
|
)
|
|
274
306
|
end
|
|
275
307
|
|
|
@@ -796,7 +828,7 @@ module Rigor
|
|
|
796
828
|
collect_attr_call(node, prefix, in_singleton_class, ctx)
|
|
797
829
|
end
|
|
798
830
|
|
|
799
|
-
node.
|
|
831
|
+
node.rigor_each_child { |child| walk_attr_calls(child, prefix, in_singleton_class, ctx) }
|
|
800
832
|
end
|
|
801
833
|
|
|
802
834
|
def collect_attr_call(call_node, prefix, in_singleton_class, ctx)
|
|
@@ -872,7 +904,7 @@ module Rigor
|
|
|
872
904
|
return
|
|
873
905
|
end
|
|
874
906
|
|
|
875
|
-
node.
|
|
907
|
+
node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
|
|
876
908
|
end
|
|
877
909
|
|
|
878
910
|
# Derive { attr_name_sym => Type } for a single `def initialize` by matching `@ivar = param_name`
|
|
@@ -950,7 +982,7 @@ module Rigor
|
|
|
950
982
|
node.is_a?(Prism::ClassNode) ||
|
|
951
983
|
node.is_a?(Prism::ModuleNode)
|
|
952
984
|
|
|
953
|
-
node.
|
|
985
|
+
node.rigor_each_child { |c| scan_ivar_param_assignments(c, param_names, result) }
|
|
954
986
|
end
|
|
955
987
|
|
|
956
988
|
def build_attr_candidates(call_name, class_name, attr_name, ivar_type, ctx)
|
|
@@ -6,6 +6,7 @@ require_relative "../environment"
|
|
|
6
6
|
require_relative "../scope"
|
|
7
7
|
require_relative "../type"
|
|
8
8
|
require_relative "../source/literals"
|
|
9
|
+
require_relative "../source/node_children"
|
|
9
10
|
require_relative "../inference/scope_indexer"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
@@ -126,7 +127,7 @@ module Rigor
|
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
129
|
|
|
129
|
-
node.
|
|
130
|
+
node.rigor_each_child { |child| walk_class_decls(child, prefix, accumulator) }
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
def qualified_constant_path(constant_path)
|
|
@@ -146,7 +147,7 @@ module Rigor
|
|
|
146
147
|
return unless node.is_a?(Prism::Node)
|
|
147
148
|
|
|
148
149
|
record_call(node, scope_index, bindings, observations) if node.is_a?(Prism::CallNode)
|
|
149
|
-
node.
|
|
150
|
+
node.rigor_each_child { |child| walk_calls(child, scope_index, bindings, observations) }
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
def record_call(call_node, scope_index, bindings, observations)
|
|
@@ -244,7 +245,7 @@ module Rigor
|
|
|
244
245
|
recognise_describe(node, bindings)
|
|
245
246
|
recognise_subject_or_let(node, bindings, scope_index)
|
|
246
247
|
|
|
247
|
-
node.
|
|
248
|
+
node.rigor_each_child { |child| walk_rspec_bindings(child, bindings, scope_index) }
|
|
248
249
|
end
|
|
249
250
|
|
|
250
251
|
def recognise_describe(node, bindings)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module SigGen
|
|
7
|
+
# Parses what sig-gen is about to emit, before it is emitted.
|
|
8
|
+
#
|
|
9
|
+
# Two rendering bugs have shipped RBS that `rbs` itself rejects — a non-identifier record key
|
|
10
|
+
# (`{ :"data-contrast" => T }`) and a `&block` constructor parameter rendered before the parens
|
|
11
|
+
# (`(**untyped, ?{ (?) -> void })`). Both were found downstream, as a poisoned `sig/` tree: the file is
|
|
12
|
+
# quarantined, its types vanish, and (for a project adopting `reject-unparseable-signatures`) the build
|
|
13
|
+
# fails. A generator that can poison its own consumer's build is worth guarding at the source.
|
|
14
|
+
#
|
|
15
|
+
# So this is not a check for ONE bug — it is the guard that turns the whole rendering-bug *class* from a
|
|
16
|
+
# silent bad artifact into a caught error naming the method that could not be rendered. It is cheap: the
|
|
17
|
+
# text is already in memory, and `RBS::Parser` is the same C parser the consumer will use, so it is exactly
|
|
18
|
+
# the oracle that matters (not an approximation of it).
|
|
19
|
+
module RbsValidity
|
|
20
|
+
# A rendered method line (`def foo: () -> String`) is a fragment, not a compilation unit — it has to be
|
|
21
|
+
# wrapped to be parsed at all. The wrapper name is irrelevant to the parse and never reaches disk.
|
|
22
|
+
PROBE_CLASS = "RigorSigGenProbe"
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param line [String] a rendered RBS method line, e.g. `def self.parse: (String) -> Integer`.
|
|
27
|
+
# @return [String, nil] the parse error's first line, or nil when the line is valid.
|
|
28
|
+
def method_line_error(line)
|
|
29
|
+
return nil if line.nil?
|
|
30
|
+
|
|
31
|
+
source_error("class #{PROBE_CLASS}\n #{line}\nend\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param source [String] a complete `.rbs` file's text.
|
|
35
|
+
# @return [String, nil] the parse error's first line, or nil when the file is valid.
|
|
36
|
+
def source_error(source)
|
|
37
|
+
::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor sig-gen)", content: source))
|
|
38
|
+
nil
|
|
39
|
+
rescue ::RBS::ParsingError => e
|
|
40
|
+
e.message.to_s.lines.first.to_s.strip
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -126,6 +126,7 @@ module Rigor
|
|
|
126
126
|
when :created then render_write_created(result)
|
|
127
127
|
when :updated then render_write_updated(result)
|
|
128
128
|
when :skipped_outside_sig_root then render_write_skipped(result)
|
|
129
|
+
when :skipped_invalid_rbs then render_write_invalid(result)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
end
|
|
@@ -143,6 +144,15 @@ module Rigor
|
|
|
143
144
|
@out.puts("skipped #{result.source_path} -> #{result.target_path} (outside sig root)")
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
# The assembled file does not parse, so it was NOT written. Writing it would poison the sig tree — the
|
|
148
|
+
# consumer quarantines an unparseable `.rbs` whole, taking every other type in that file down with it,
|
|
149
|
+
# including the user's own hand-written ones.
|
|
150
|
+
def render_write_invalid(result)
|
|
151
|
+
@out.puts("REFUSED #{result.target_path} — the generated RBS does not parse, so it was not written")
|
|
152
|
+
@out.puts(" #{result.error}")
|
|
153
|
+
@out.puts(" This is a bug in Rigor's RBS rendering, not in your code — please report it.")
|
|
154
|
+
end
|
|
155
|
+
|
|
146
156
|
def render_write_json(results)
|
|
147
157
|
@out.puts(JSON.pretty_generate({ results: results.map(&:to_h) }))
|
|
148
158
|
end
|
|
@@ -10,19 +10,24 @@ module Rigor
|
|
|
10
10
|
# - `source_path` — original `.rb` file.
|
|
11
11
|
# - `target_path` — `.rbs` file the writer was responsible for (`nil` when the source path falls outside
|
|
12
12
|
# the project signature tree, in which case `action` is `:skipped_outside_sig_root`).
|
|
13
|
-
# - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root
|
|
13
|
+
# - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root` /
|
|
14
|
+
# `:skipped_invalid_rbs`.
|
|
14
15
|
# - `applied` — the {MethodCandidate}s that actually landed on disk.
|
|
15
16
|
# - `skipped` — the {MethodCandidate}s the writer declined (e.g. tighter-return without `--overwrite`). Each
|
|
16
17
|
# entry pairs the candidate with a skip reason keyword (`:user_authored`).
|
|
18
|
+
# - `error` — the parse error, when `action` is `:skipped_invalid_rbs`: the file the writer assembled does
|
|
19
|
+
# not parse, so it was NOT written (writing it would poison the project's sig tree — the consumer
|
|
20
|
+
# quarantines an unparseable `.rbs`, taking every other type in that file down with it).
|
|
17
21
|
class WriteResult
|
|
18
|
-
attr_reader :source_path, :target_path, :action, :applied, :skipped
|
|
22
|
+
attr_reader :source_path, :target_path, :action, :applied, :skipped, :error
|
|
19
23
|
|
|
20
|
-
def initialize(source_path:, target_path:, action:, applied: [], skipped: [])
|
|
24
|
+
def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil)
|
|
21
25
|
@source_path = source_path
|
|
22
26
|
@target_path = target_path
|
|
23
27
|
@action = action
|
|
24
28
|
@applied = applied.freeze
|
|
25
29
|
@skipped = skipped.freeze
|
|
30
|
+
@error = error
|
|
26
31
|
freeze
|
|
27
32
|
end
|
|
28
33
|
|
|
@@ -33,7 +38,7 @@ module Rigor
|
|
|
33
38
|
action: action.to_s,
|
|
34
39
|
applied: applied.map(&:to_h),
|
|
35
40
|
skipped: skipped.map { |c, reason| c.to_h.merge(write_skip_reason: reason.to_s) }
|
|
36
|
-
}
|
|
41
|
+
}.tap { |h| h[:error] = error if error }
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
end
|
data/lib/rigor/sig_gen/writer.rb
CHANGED
|
@@ -108,8 +108,15 @@ module Rigor
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def create_new(source_path, target, candidates)
|
|
111
|
+
content = render_new_file(candidates)
|
|
112
|
+
error = RbsValidity.source_error(content)
|
|
113
|
+
if error
|
|
114
|
+
return WriteResult.new(source_path: source_path, target_path: target,
|
|
115
|
+
action: :skipped_invalid_rbs, error: error)
|
|
116
|
+
end
|
|
117
|
+
|
|
111
118
|
FileUtils.mkdir_p(target.dirname)
|
|
112
|
-
target.write(
|
|
119
|
+
target.write(content)
|
|
113
120
|
WriteResult.new(source_path: source_path, target_path: target,
|
|
114
121
|
action: :created, applied: candidates)
|
|
115
122
|
end
|
|
@@ -277,7 +284,22 @@ module Rigor
|
|
|
277
284
|
merge_class_shells(state, collect_class_shells(candidates), merged_namespace_kinds(candidates))
|
|
278
285
|
|
|
279
286
|
action = state.applied.empty? ? :noop : :updated
|
|
280
|
-
|
|
287
|
+
unless action == :updated
|
|
288
|
+
return WriteResult.new(source_path: source_path, target_path: target, action: action,
|
|
289
|
+
applied: state.applied, skipped: state.skipped)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# The merge splices text into an existing file by byte offset, so a bug here can produce a file neither
|
|
293
|
+
# the generator nor the target was responsible for. Parse the assembled result and refuse to write it if
|
|
294
|
+
# it is broken: a `.rbs` the consumer cannot parse is quarantined whole, so a bad splice would delete
|
|
295
|
+
# every type in the file — including the user's own, which were fine before we touched them.
|
|
296
|
+
error = RbsValidity.source_error(state.source)
|
|
297
|
+
if error
|
|
298
|
+
return WriteResult.new(source_path: source_path, target_path: target,
|
|
299
|
+
action: :skipped_invalid_rbs, error: error)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
target.write(state.source)
|
|
281
303
|
WriteResult.new(source_path: source_path, target_path: target,
|
|
282
304
|
action: action, applied: state.applied, skipped: state.skipped)
|
|
283
305
|
end
|