rigortype 0.4.1 → 0.4.3
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 +1 -1
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/11-sig-gen.md +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +59 -12
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- data/lib/rigor/inference/module_function_state.rb +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +34 -2
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -72,6 +72,7 @@ module Rigor
|
|
|
72
72
|
)
|
|
73
73
|
runner = invocation.runner
|
|
74
74
|
raw_result = invocation.result
|
|
75
|
+
settle_run_stats(raw_result.stats)
|
|
75
76
|
result = apply_baseline_filter(raw_result, configuration, options)
|
|
76
77
|
|
|
77
78
|
coverage = compute_coverage(runner, configuration, options)
|
|
@@ -180,9 +181,9 @@ module Rigor
|
|
|
180
181
|
# A plain `--incremental` run the session would answer as a warm recheck and write a slot for. Every other
|
|
181
182
|
# shape declines, and the full path handles it exactly as before: an editor buffer (never saved), `--no-cache`
|
|
182
183
|
# (no store to hold a slot), `--verify-incremental`, a worker pool (the session writes no slot for one: a
|
|
183
|
-
# worker's plugin reads never reach the process that records them), and `--explain
|
|
184
|
-
#
|
|
185
|
-
# its writer never saw. The rest mirror the plain probe: `--coverage`, `--cache-stats` and the
|
|
184
|
+
# worker's plugin reads never reach the process that records them), and `--explain` (#1533: the session
|
|
185
|
+
# writes no slot for it — the slot key has no explain slot — so the probe declines rather than answer for a
|
|
186
|
+
# flag its writer never saw). The rest mirror the plain probe: `--coverage`, `--cache-stats` and the
|
|
186
187
|
# `RIGOR_*_TRACE` probes each want the full path's own output.
|
|
187
188
|
def incremental_run_hit_eligible?(configuration, options, buffer)
|
|
188
189
|
options.fetch(:incremental) && buffer.nil? &&
|
|
@@ -195,6 +196,7 @@ module Rigor
|
|
|
195
196
|
# The tail {#run_incremental_check} gives a warm run, from the slot's answer: the same banner, baseline
|
|
196
197
|
# filter, output and exit code. No fact-surface note, since a warm run never has one.
|
|
197
198
|
def finalize_incremental_hit(hit, configuration, options)
|
|
199
|
+
# The slot is written only for a run whose every row still validates, so nothing was re-analyzed.
|
|
198
200
|
report_incremental_run(true, hit.file_count)
|
|
199
201
|
write_incremental_result(hit.result.diagnostics, configuration, options)
|
|
200
202
|
end
|
|
@@ -267,8 +269,8 @@ module Rigor
|
|
|
267
269
|
# Every other file forms the re-analyzed subset, so the run exercises BOTH the subset-analysis path and the
|
|
268
270
|
# cache-serving path.
|
|
269
271
|
subset = analyzed.each_with_index.select { |_, index| index.even? }.map(&:first)
|
|
270
|
-
incremental =
|
|
271
|
-
full =
|
|
272
|
+
incremental = session.reanalyze_subset(subset).map(&:to_h)
|
|
273
|
+
full = verify_full_diagnostics(configuration, paths, collect_stats: options.fetch(:stats)).map(&:to_h)
|
|
272
274
|
|
|
273
275
|
report_verify_incremental(incremental, full, subset_size: subset.size, total: analyzed.size,
|
|
274
276
|
format: options.fetch(:format))
|
|
@@ -288,10 +290,11 @@ module Rigor
|
|
|
288
290
|
nil
|
|
289
291
|
else
|
|
290
292
|
Cache::IncrementalSnapshot.fingerprint(
|
|
291
|
-
configuration: configuration, roots: paths || configuration.paths
|
|
293
|
+
configuration: configuration, roots: paths || configuration.paths,
|
|
294
|
+
explain: options.fetch(:explain)
|
|
292
295
|
)
|
|
293
296
|
end
|
|
294
|
-
snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
|
|
297
|
+
snapshot = Cache::IncrementalSnapshot.new(root: cache_root, explain: options.fetch(:explain))
|
|
295
298
|
store = incremental_cache_store(configuration, options, cache_root)
|
|
296
299
|
session = Analysis::IncrementalSession.new(
|
|
297
300
|
configuration: configuration, paths: paths,
|
|
@@ -300,7 +303,7 @@ module Rigor
|
|
|
300
303
|
# (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
|
|
301
304
|
# re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
|
|
302
305
|
workers: CheckRunnerFactory.resolve_workers(options, configuration),
|
|
303
|
-
buffer: buffer
|
|
306
|
+
buffer: buffer, explain: options.fetch(:explain)
|
|
304
307
|
)
|
|
305
308
|
|
|
306
309
|
return run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options) if buffer
|
|
@@ -309,7 +312,8 @@ module Rigor
|
|
|
309
312
|
# The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
|
|
310
313
|
# `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
|
|
311
314
|
# built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
|
|
312
|
-
|
|
315
|
+
# A warm banner reports the recheck's own sets (#1526), which only the session knows.
|
|
316
|
+
report_incremental_run(warm, session.analyzed_files.size, recheck: session.last_recheck)
|
|
313
317
|
emit_incremental_fact_surface_notes(session)
|
|
314
318
|
write_incremental_cache_stats(session, cache_root, store, options.fetch(:format)) if
|
|
315
319
|
options.fetch(:cache_stats)
|
|
@@ -317,9 +321,47 @@ module Rigor
|
|
|
317
321
|
write_incremental_result(diagnostics, configuration, options)
|
|
318
322
|
end
|
|
319
323
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
# #1526 — a warm banner says how many of the analysed files the recheck re-analysed, and why: `changed` is the
|
|
325
|
+
# edited set, `added` / `removed` the structural delta, and the rest came from the snapshot. A run served from
|
|
326
|
+
# the run-result slot has no recheck, which means nothing changed: every file is reused.
|
|
327
|
+
def report_incremental_run(warm, file_count, recheck: nil)
|
|
328
|
+
body = if warm
|
|
329
|
+
"warm — #{warm_recheck_summary(file_count, recheck)}"
|
|
330
|
+
else
|
|
331
|
+
"cold — full analysis (#{pluralize(file_count, 'file')})"
|
|
332
|
+
end
|
|
333
|
+
@err.puts("rigor: --incremental #{body}")
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The warm clause. `file_count` is the analysed set M (the subset under a path argument); re-analysed files
|
|
337
|
+
# are M minus the reused set, which includes files invalidated by inferred-parameter changes (ADR-67), not only
|
|
338
|
+
# the edited closure. View templates are not in M: the recheck re-runs every one, so they are appended as their
|
|
339
|
+
# own count rather than folded into the file counts.
|
|
340
|
+
def warm_recheck_summary(file_count, recheck)
|
|
341
|
+
reused = recheck ? recheck.reused.size : file_count
|
|
342
|
+
# Current files are either reused or re-analysed (the recheck's `analyze_set`), so the count is taken from
|
|
343
|
+
# the reused set rather than `affected`, which can also name files that were removed.
|
|
344
|
+
analyzed = file_count - reused
|
|
345
|
+
head = "re-analysed #{analyzed} of #{pluralize(file_count, 'file')}"
|
|
346
|
+
head += " (#{change_counts(recheck).join(', ')})" if recheck && !change_counts(recheck).empty?
|
|
347
|
+
head += reuse_clause(analyzed, reused)
|
|
348
|
+
templates = recheck ? recheck.templates : 0
|
|
349
|
+
templates.positive? ? "#{head}, plus #{pluralize(templates, 'view template')}" : head
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def change_counts(recheck)
|
|
353
|
+
{ "changed" => recheck.changed.size, "added" => recheck.added.size, "removed" => recheck.removed.size }
|
|
354
|
+
.filter_map { |label, count| "#{count} #{label}" if count.positive? }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def reuse_clause(analyzed, reused)
|
|
358
|
+
return ", reused all" if analyzed.zero?
|
|
359
|
+
|
|
360
|
+
reused.zero? ? "" : ", reused the rest"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def pluralize(count, noun)
|
|
364
|
+
"#{count} #{count == 1 ? noun : "#{noun}s"}"
|
|
323
365
|
end
|
|
324
366
|
|
|
325
367
|
# The incremental tail, shared by the full path and the ADR-45 WD2 slot hit so a served run prints exactly
|
|
@@ -366,17 +408,30 @@ module Rigor
|
|
|
366
408
|
|
|
367
409
|
# ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
|
|
368
410
|
# value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
|
|
369
|
-
# snapshot (a Sorbet sig edit, a schema change) or a
|
|
370
|
-
#
|
|
371
|
-
#
|
|
411
|
+
# snapshot (a Sorbet sig edit, a schema change) or a plugin is opaque to the fingerprint (a full run every
|
|
412
|
+
# invocation): it contributes types and declares NO fingerprint surface, or a value it declares cannot be
|
|
413
|
+
# digested. Each opaque plugin's reason is worded per {PluginFactFingerprint::OpaqueReason}, and an
|
|
414
|
+
# undigestible value is named. Both are the sound direction; the note explains why a recheck was not warm.
|
|
372
415
|
def emit_incremental_fact_surface_notes(session)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
416
|
+
reasons = session.opaque_reasons
|
|
417
|
+
surfaceless = reasons.keys.select { |id| reasons.fetch(id).kind == :no_surface }.sort
|
|
418
|
+
unless surfaceless.empty?
|
|
419
|
+
many = surfaceless.size > 1
|
|
420
|
+
@err.puts("rigor: --incremental plugin#{'s' if many} #{surfaceless.join(', ')} " \
|
|
421
|
+
"contribute#{'s' unless many} call-site types with no incremental fingerprint surface; " \
|
|
422
|
+
"the snapshot cannot be reused (full analysis every run).")
|
|
423
|
+
end
|
|
424
|
+
reasons.each do |id, reason|
|
|
425
|
+
next if reason.kind == :no_surface
|
|
426
|
+
|
|
427
|
+
@err.puts("rigor: --incremental #{reason.describe(id)}; the snapshot cannot be reused " \
|
|
428
|
+
"(full analysis every run).")
|
|
429
|
+
end
|
|
430
|
+
if session.fact_store_unreadable?
|
|
431
|
+
@err.puts("rigor: --incremental the plugin fact store could not be read; the snapshot cannot be reused " \
|
|
432
|
+
"(full analysis every run).")
|
|
378
433
|
end
|
|
379
|
-
return unless session.fact_surface_invalidated? &&
|
|
434
|
+
return unless session.fact_surface_invalidated? && !session.opaque_fact_surface?
|
|
380
435
|
|
|
381
436
|
@err.puts("rigor: --incremental plugin fact surface changed since the snapshot; ran a full analysis.")
|
|
382
437
|
end
|
|
@@ -388,8 +443,10 @@ module Rigor
|
|
|
388
443
|
def write_incremental_cache_stats(session, cache_root, store, format)
|
|
389
444
|
write_cache_stats(cache_root, store, format)
|
|
390
445
|
status =
|
|
391
|
-
if
|
|
392
|
-
|
|
446
|
+
if session.opaque_fact_surface?
|
|
447
|
+
verdicts = session.opaque_reasons.map { |id, reason| "#{id}: #{reason.summary}" }
|
|
448
|
+
verdicts << "fact store: could not be read" if session.fact_store_unreadable?
|
|
449
|
+
"opaque (#{verdicts.join('; ')})"
|
|
393
450
|
elsif session.fact_surface_invalidated?
|
|
394
451
|
"changed — snapshot invalidated"
|
|
395
452
|
else
|
|
@@ -398,38 +455,72 @@ module Rigor
|
|
|
398
455
|
side_output(format).puts(" plugin fact surface: #{status}")
|
|
399
456
|
end
|
|
400
457
|
|
|
401
|
-
def verify_full_diagnostics(configuration, paths)
|
|
402
|
-
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil)
|
|
458
|
+
def verify_full_diagnostics(configuration, paths, collect_stats: true)
|
|
459
|
+
runner = Analysis::Runner.new(configuration: configuration, cache_store: nil, collect_stats: collect_stats)
|
|
403
460
|
(paths ? runner.run(paths) : runner.run).diagnostics
|
|
404
461
|
end
|
|
405
462
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
463
|
+
# Issue #1542 — the rows are compared as a multiset over every field each row carries, so two rows that tie on
|
|
464
|
+
# the sort key but differ in severity, or a duplicated row, are a set mismatch and never a silent 0/0. Only when
|
|
465
|
+
# the multisets agree is the order compared, and an order mismatch is reported as its own failure, with the first
|
|
466
|
+
# index at which the two lists diverge, so a set regression and an order regression stay distinguishable.
|
|
467
|
+
#
|
|
412
468
|
# The OK line follows the same rule as the other side outputs (#493) — stdout under `text`, stderr
|
|
413
469
|
# under a machine format. The FAILED branch below has always gone to stderr: it is a failure
|
|
414
470
|
# report, and a run that produced one has no valid document to protect anyway.
|
|
415
471
|
def report_verify_incremental(incremental, full, subset_size:, total:, format: "text")
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
472
|
+
return report_verify_set_mismatch(incremental, full) unless incremental.tally == full.tally
|
|
473
|
+
return report_verify_order_mismatch(incremental, full) unless incremental == full
|
|
474
|
+
|
|
475
|
+
side_output(format).puts("rigor: --verify-incremental OK — incremental " \
|
|
476
|
+
"(#{subset_size}/#{total} files re-analyzed, rest from cache) " \
|
|
477
|
+
"matches full (#{full.size} diagnostics)")
|
|
478
|
+
0
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# The rows one side has more of than the other, counted by multiplicity: [x, x, y] against [x, y, y] has one
|
|
482
|
+
# extra x on the left and one extra y on the right.
|
|
483
|
+
def multiset_difference(left, right)
|
|
484
|
+
budget = right.tally
|
|
485
|
+
left.reject do |row|
|
|
486
|
+
next false unless budget.fetch(row, 0).positive?
|
|
487
|
+
|
|
488
|
+
budget[row] -= 1
|
|
489
|
+
true
|
|
421
490
|
end
|
|
491
|
+
end
|
|
422
492
|
|
|
423
|
-
|
|
424
|
-
|
|
493
|
+
def report_verify_set_mismatch(incremental, full)
|
|
494
|
+
only_incremental = multiset_difference(incremental, full)
|
|
495
|
+
only_full = multiset_difference(full, incremental)
|
|
425
496
|
@err.puts("rigor: --verify-incremental FAILED — incremental and full diagnostics differ.")
|
|
426
497
|
@err.puts(" incremental-only: #{only_incremental.size}, full-only: #{only_full.size}")
|
|
427
498
|
(only_incremental + only_full).first(10).each do |hash|
|
|
428
|
-
@err.puts(" #{hash
|
|
499
|
+
@err.puts(" #{diagnostic_location(hash)}")
|
|
429
500
|
end
|
|
430
501
|
1
|
|
431
502
|
end
|
|
432
503
|
|
|
504
|
+
def report_verify_order_mismatch(incremental, full)
|
|
505
|
+
index = (0...incremental.size).find { |i| incremental[i] != full[i] }
|
|
506
|
+
@err.puts("rigor: --verify-incremental FAILED — same set, different order.")
|
|
507
|
+
@err.puts(" incremental and full report the same #{full.size} diagnostics in a different order; " \
|
|
508
|
+
"first divergence at index #{index}:")
|
|
509
|
+
@err.puts(" incremental: #{diagnostic_location(incremental[index])}")
|
|
510
|
+
@err.puts(" full: #{diagnostic_location(full[index])}")
|
|
511
|
+
1
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Severity, source family and the optional fields a row may carry, so two rows that differ only in one of them
|
|
515
|
+
# never print as the same line.
|
|
516
|
+
def diagnostic_location(hash)
|
|
517
|
+
extra = %w[receiver_type method_name project_definition_site].filter_map do |key|
|
|
518
|
+
" #{key}=#{hash[key]}" if hash.key?(key)
|
|
519
|
+
end
|
|
520
|
+
"#{hash['path']}:#{hash['line']}:#{hash['column']}: #{hash['severity']} [#{hash['rule']}] " \
|
|
521
|
+
"(#{hash['source_family']}) #{hash['message']}#{extra.join}"
|
|
522
|
+
end
|
|
523
|
+
|
|
433
524
|
# ADR-22 slice 5 — the `--baseline-strict` CI gate. When the flag is set, ANY baseline drift fails the run — not
|
|
434
525
|
# only excess drift (a bucket over threshold, which already fails via the surfaced diagnostics) but also DEFICIT
|
|
435
526
|
# drift (`actual < count`: the baseline has grown looser than the code and should be regenerated). A no-op, with a
|
|
@@ -685,9 +776,11 @@ module Rigor
|
|
|
685
776
|
# ADR-103 WD15 — captured before `DEFAULTS.merge` below folds "no `effects:` key" and "`effects:
|
|
686
777
|
# false`" into the same value; see `Configuration.load`'s identical capture.
|
|
687
778
|
effects_key_present = data.key?("effects")
|
|
779
|
+
target_ruby_key_present = data.key?("target_ruby")
|
|
688
780
|
data = data.dup
|
|
689
781
|
data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
|
|
690
|
-
Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present)
|
|
782
|
+
Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present, target_ruby_key_present)
|
|
783
|
+
.with_exclude_base(path && File.exist?(path) ? File.dirname(path) : nil)
|
|
691
784
|
end
|
|
692
785
|
|
|
693
786
|
# ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
|
|
@@ -724,6 +817,13 @@ module Rigor
|
|
|
724
817
|
end
|
|
725
818
|
end
|
|
726
819
|
|
|
820
|
+
# Issue #1529 — reads the run's peak RSS straight after the analysis, before coverage or any output, so the
|
|
821
|
+
# figure the summary prints (and the JSON `stats` block carries) covers the analysis alone. A nil `stats`
|
|
822
|
+
# (`--no-stats`, or a run served from a cache) reads nothing.
|
|
823
|
+
def settle_run_stats(stats)
|
|
824
|
+
stats&.peak_rss_bytes
|
|
825
|
+
end
|
|
826
|
+
|
|
727
827
|
# Emits the {Analysis::RunStats} summary to STDERR so it doesn't interleave with the diagnostic stream (text or
|
|
728
828
|
# JSON) on STDOUT. JSON consumers can pipe stdout cleanly; interactive users still see the summary on their tty.
|
|
729
829
|
def write_run_stats(stats)
|
|
@@ -78,7 +78,7 @@ module Rigor
|
|
|
78
78
|
|
|
79
79
|
def run_index
|
|
80
80
|
if File.file?(LLMS_INDEX)
|
|
81
|
-
@out.write(File.read(LLMS_INDEX))
|
|
81
|
+
@out.write(File.read(LLMS_INDEX, encoding: Encoding::UTF_8))
|
|
82
82
|
0
|
|
83
83
|
else
|
|
84
84
|
run_list(nil)
|
|
@@ -115,11 +115,12 @@ module Rigor
|
|
|
115
115
|
doc = resolve_doc(name)
|
|
116
116
|
return doc if doc.is_a?(Integer) # error status, already reported
|
|
117
117
|
|
|
118
|
-
#
|
|
119
|
-
#
|
|
118
|
+
# The bundled page is UTF-8 by contract, so it is read as UTF-8 explicitly: under a C or POSIX locale the
|
|
119
|
+
# process default external encoding is US-ASCII, and a non-ASCII body would otherwise raise in `gsub`.
|
|
120
|
+
# The provenance header stays ASCII-only so it never disagrees with the rest of the output.
|
|
120
121
|
@out.puts("<!-- rigor docs #{doc.fetch(:name)} (rigortype #{Rigor::VERSION}, offline) -->")
|
|
121
122
|
@out.puts
|
|
122
|
-
@out.write(DocLinks.rewrite(File.read(doc.fetch(:path)), from: doc.fetch(:path)))
|
|
123
|
+
@out.write(DocLinks.rewrite(File.read(doc.fetch(:path), encoding: Encoding::UTF_8), from: doc.fetch(:path)))
|
|
123
124
|
0
|
|
124
125
|
end
|
|
125
126
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "command"
|
|
7
|
+
require_relative "options"
|
|
8
|
+
require_relative "check_runner_factory"
|
|
9
|
+
require_relative "../configuration"
|
|
10
|
+
require_relative "../lens"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
class CLI
|
|
14
|
+
# `rigor lens FILE...` — a declaration map of the files with type provenance, computed as `rigor check`
|
|
15
|
+
# computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
|
|
16
|
+
class LensCommand < Command
|
|
17
|
+
USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
|
|
18
|
+
FORMATS = %w[text json].freeze
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
options = parse_options
|
|
22
|
+
return CLI::EXIT_USAGE if options.nil?
|
|
23
|
+
|
|
24
|
+
config = options.fetch(:config)
|
|
25
|
+
if config && !File.file?(config)
|
|
26
|
+
usage_error("no configuration file at #{config}")
|
|
27
|
+
return CLI::EXIT_USAGE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
configuration = Configuration.load(config)
|
|
31
|
+
config_path = options.fetch(:config) || Configuration.discover
|
|
32
|
+
# The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
|
|
33
|
+
root = Dir.pwd
|
|
34
|
+
workers = CheckRunnerFactory.resolve_workers(options, configuration)
|
|
35
|
+
outcome = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers).call
|
|
36
|
+
document = Lens::Document.new(outcome: outcome, configuration: configuration, root: root,
|
|
37
|
+
config_path: config_path).to_h
|
|
38
|
+
@err.puts("rigor lens: #{outcome.warm ? 'warm' : 'cold'} — #{@argv.size} target(s)")
|
|
39
|
+
render(document, options.fetch(:format))
|
|
40
|
+
outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def parse_options
|
|
46
|
+
options = { format: "text", config: nil, workers: nil }
|
|
47
|
+
OptionParser.new do |opts|
|
|
48
|
+
opts.banner = USAGE
|
|
49
|
+
opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
|
|
50
|
+
opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
|
|
51
|
+
Options.add_config(opts, options)
|
|
52
|
+
end.parse!(@argv)
|
|
53
|
+
return usage_error("--format must be text or json") unless FORMATS.include?(options[:format])
|
|
54
|
+
return usage_error("no FILE given") if @argv.empty?
|
|
55
|
+
|
|
56
|
+
options
|
|
57
|
+
rescue OptionParser::ParseError => e
|
|
58
|
+
usage_error(e.message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def usage_error(message)
|
|
62
|
+
@err.puts("rigor lens: #{message}")
|
|
63
|
+
@err.puts(USAGE)
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render(document, format)
|
|
68
|
+
if format == "json"
|
|
69
|
+
@out.puts(JSON.generate(document))
|
|
70
|
+
else
|
|
71
|
+
TextRenderer.new(document, @out).render
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The text rendering for agents and people. Its layout is not a contract; the `LINE:HASH` anchor token is.
|
|
76
|
+
class TextRenderer
|
|
77
|
+
def initialize(document, out)
|
|
78
|
+
@document = document
|
|
79
|
+
@out = out
|
|
80
|
+
@members = document["members"].each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |member, index|
|
|
81
|
+
member["sites"].each { |key| index[key] << member }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render
|
|
86
|
+
@document["files"].each { |file| render_file(file) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def render_file(file)
|
|
92
|
+
unless file["status"] == "ok"
|
|
93
|
+
@out.puts("#{file['argument']} #{file['status']}: #{file['reason']}")
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
omitted = file["omissions"].empty? ? "" : " (#{file['omissions'].size} omitted)"
|
|
98
|
+
@out.puts("#{file['path']} #{file['digest']}#{omitted}")
|
|
99
|
+
file["sites"].each { |site| render_site(site) }
|
|
100
|
+
file["omissions"].each do |omission|
|
|
101
|
+
@out.puts(" omitted #{omission['kind']} #{omission['name']} (line #{omission.dig('span', 'start_line')})")
|
|
102
|
+
end
|
|
103
|
+
file["diagnostics"].each do |diagnostic|
|
|
104
|
+
@out.puts(" #{diagnostic['line']}:#{diagnostic['column']} #{diagnostic['severity']} " \
|
|
105
|
+
"#{diagnostic['message']} [#{diagnostic['rule']}]")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_site(site)
|
|
110
|
+
case site["kind"]
|
|
111
|
+
when "class", "module" then @out.puts(" #{site['anchor']} #{site['kind']} #{site['name']}")
|
|
112
|
+
else
|
|
113
|
+
@members[site["key"]].each { |member| render_member(site, member) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_member(site, member)
|
|
118
|
+
separator = member["scope"] == "singleton" ? "." : "#"
|
|
119
|
+
if member["overloads"].empty?
|
|
120
|
+
elsewhere = member["also_defined_at"].map { |entry| "#{entry['path']}:#{entry['line']}" }
|
|
121
|
+
@out.puts(" #{site['anchor']} #{site['owner']}#{separator}#{member['name']} " \
|
|
122
|
+
"[#{member['basis']}#{"; also #{elsewhere.join(', ')}" unless elsewhere.empty?}]")
|
|
123
|
+
end
|
|
124
|
+
member["overloads"].each do |overload|
|
|
125
|
+
signature = overload["rbs"] || "(signature unavailable: #{unavailable_reasons(overload).join(', ')})"
|
|
126
|
+
@out.puts(" #{site['anchor']} #{site['owner']}#{separator}#{member['name']}: #{signature} " \
|
|
127
|
+
"[#{member['basis']}#{provenance_note(overload)}]")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def provenance_note(overload)
|
|
132
|
+
provenances = ([overload["return"]] + (overload["params"] || []).map { |param| param["type"] })
|
|
133
|
+
.filter_map { |slot| slot["provenance"] }.uniq
|
|
134
|
+
provenances.empty? ? "" : " #{provenances.join('+')}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def unavailable_reasons(overload)
|
|
138
|
+
([overload["return"]] + (overload["params"] || []).map { |param| param["type"] })
|
|
139
|
+
.filter_map { |slot| slot["reason"] }.uniq
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -53,8 +53,11 @@ module Rigor
|
|
|
53
53
|
def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
|
|
54
54
|
configuration = Configuration.load(config_path)
|
|
55
55
|
# ProjectContext caches Environment + Cache::Store across requests so hover / publish hit the warm path.
|
|
56
|
-
# Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration
|
|
57
|
-
|
|
56
|
+
# Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration`; every save round
|
|
57
|
+
# also re-reads the configuration (the loader below) and re-baselines when its inputs moved (#1613).
|
|
58
|
+
project_context = LanguageServer::ProjectContext.new(
|
|
59
|
+
configuration: configuration, configuration_loader: -> { Configuration.load(config_path) }
|
|
60
|
+
)
|
|
58
61
|
# Single source of truth for buffer state — threaded to Server + all three providers.
|
|
59
62
|
buffer_table = LanguageServer::BufferTable.new
|
|
60
63
|
debouncer = LanguageServer::Debouncer.new
|
|
@@ -128,7 +128,7 @@ module Rigor
|
|
|
128
128
|
|
|
129
129
|
@out.puts(render_print_header(skill))
|
|
130
130
|
@out.puts
|
|
131
|
-
@out.write(File.read(skill.fetch(:path)))
|
|
131
|
+
@out.write(File.read(skill.fetch(:path), encoding: Encoding::UTF_8))
|
|
132
132
|
0
|
|
133
133
|
end
|
|
134
134
|
|
|
@@ -145,14 +145,14 @@ module Rigor
|
|
|
145
145
|
|
|
146
146
|
@out.puts(render_print_header(skill))
|
|
147
147
|
@out.puts
|
|
148
|
-
@out.write(File.read(skill.fetch(:path)))
|
|
148
|
+
@out.write(File.read(skill.fetch(:path), encoding: Encoding::UTF_8))
|
|
149
149
|
|
|
150
150
|
reference_files(skill).each do |ref|
|
|
151
151
|
@out.puts
|
|
152
152
|
@out.puts
|
|
153
153
|
@out.puts("<!-- ===== references/#{File.basename(ref)} (bundled with rigortype #{Rigor::VERSION}) ===== -->")
|
|
154
154
|
@out.puts
|
|
155
|
-
@out.write(File.read(ref))
|
|
155
|
+
@out.write(File.read(ref, encoding: Encoding::UTF_8))
|
|
156
156
|
end
|
|
157
157
|
0
|
|
158
158
|
end
|
|
@@ -377,7 +377,7 @@ module Rigor
|
|
|
377
377
|
# Parse a SKILL.md's leading `---`-delimited YAML frontmatter. Returns {} on any missing or malformed block so the
|
|
378
378
|
# catalogue degrades to a name-only entry rather than raising.
|
|
379
379
|
def frontmatter(path)
|
|
380
|
-
text = File.read(path)
|
|
380
|
+
text = File.read(path, encoding: Encoding::UTF_8)
|
|
381
381
|
return {} unless text.start_with?("---\n")
|
|
382
382
|
|
|
383
383
|
closing = text.index("\n---\n", 4)
|
|
@@ -69,7 +69,12 @@ module Rigor
|
|
|
69
69
|
def execute(file:, options:)
|
|
70
70
|
configuration = Configuration.load(options.fetch(:config))
|
|
71
71
|
source = File.read(file, encoding: Encoding::UTF_8)
|
|
72
|
+
# Prism parses the ORIGINAL bytes, so a `# -*- coding: -*-` file is read in its own encoding. Only the text the
|
|
73
|
+
# renderer sees is scrubbed: the colouriser and line splitter raise on a byte invalid in UTF-8 (a comment,
|
|
74
|
+
# `__END__` data, a non-UTF-8 file).
|
|
75
|
+
# Scrubbing is byte-for-byte (`?` per bad byte), so Prism's positions still address it.
|
|
72
76
|
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
77
|
+
source = source.scrub { |bytes| "?" * bytes.bytesize } unless source.valid_encoding?
|
|
73
78
|
return 1 if parse_errors?(parse_result, file)
|
|
74
79
|
|
|
75
80
|
events = record_events(parse_result.value, file, configuration)
|
|
@@ -87,7 +87,7 @@ module Rigor
|
|
|
87
87
|
# file is parsed and scope-indexed once, however many positions land in it.
|
|
88
88
|
def execute(targets:, options:, buffer: nil)
|
|
89
89
|
configuration = Configuration.load(options.fetch(:config))
|
|
90
|
-
environment = project_environment(targets.map(&:file).uniq, configuration)
|
|
90
|
+
environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
|
|
91
91
|
base_scope = Scope.empty(environment: environment)
|
|
92
92
|
|
|
93
93
|
results_by_target = Array.new(targets.length)
|
|
@@ -218,8 +218,11 @@ module Rigor
|
|
|
218
218
|
# closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
|
|
219
219
|
# roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
|
|
220
220
|
# so probes against files outside the current process's CWD still see the right `sig/` tree.
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
# Issue #1700 — an editor buffer is scanned for required features in place of the file it stands for.
|
|
222
|
+
def project_environment(files, configuration, buffer = nil)
|
|
223
|
+
patched = ProjectEnvironment.project_patched_methods(configuration, buffer: buffer)
|
|
224
|
+
ProjectEnvironment.build(configuration: configuration, source_files: files, project_patched_methods: patched,
|
|
225
|
+
feature_sources: ProjectEnvironment.buffer_sources(buffer))
|
|
223
226
|
end
|
|
224
227
|
|
|
225
228
|
def file_exists?(file)
|