rigortype 0.3.9 → 0.4.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/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "../cache/file_digest"
|
|
6
|
+
require_relative "template_unit_paths"
|
|
7
|
+
require_relative "../plugin/template_unit"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Analysis
|
|
11
|
+
# The build half of {TemplateUnits} (#392, #1038): it expands every loaded plugin's `template_globs:`,
|
|
12
|
+
# hands each match's bytes to the plugin's transform, isolates a failure into a {TemplateUnits::Failure}
|
|
13
|
+
# row, substitutes the editor's buffer for the file on disk where one is bound, and — since #1038 —
|
|
14
|
+
# carries a warm index's units for the templates that did not move.
|
|
15
|
+
#
|
|
16
|
+
# It is a separate file from the index for the reason {TemplateUnitPaths} is: the index is what the run
|
|
17
|
+
# READS, on a hot path, and the reasons the build has to be careful are longer than the build.
|
|
18
|
+
module TemplateUnitCollector
|
|
19
|
+
# Expands every loaded plugin's `template_globs:` and runs its transform.
|
|
20
|
+
#
|
|
21
|
+
# A plugin whose glob matches nothing, or whose hook returns `[]`, contributes nothing. A plugin that
|
|
22
|
+
# RAISES contributes nothing and does not break the run — the same failure isolation
|
|
23
|
+
# `#diagnostics_for_file` has, and for the same reason: a template compiler meeting a file it cannot
|
|
24
|
+
# read must cost that file's typing, never the run.
|
|
25
|
+
#
|
|
26
|
+
# Two plugins claiming one path is a conflict with no principled winner, so registration order decides
|
|
27
|
+
# and the later claim is dropped; the loser is not a diagnostic (the project chose both plugins).
|
|
28
|
+
# @param previous — a warm index from an earlier run of the same project (#1038). Every unit in it
|
|
29
|
+
# whose template is unchanged on disk is carried over instead of recompiled; see the class note.
|
|
30
|
+
def self.collect(registry:, root: Dir.pwd, buffer: nil, previous: nil)
|
|
31
|
+
claims = claims_for(registry)
|
|
32
|
+
claimed = claims.flat_map(&:last).uniq
|
|
33
|
+
signature = claims.map { |plugin, _globs| plugin_signature(plugin) }
|
|
34
|
+
collection = Collection.new(
|
|
35
|
+
root: root, buffer: buffer,
|
|
36
|
+
carried: previous&.carry_over(root: root, claimed_globs: claimed, plugin_signature: signature) || {}
|
|
37
|
+
)
|
|
38
|
+
state = { entries: {}, failures: [], stats: {} }
|
|
39
|
+
claims.each { |plugin, globs| collect_plugin(plugin, globs, collection, state) }
|
|
40
|
+
TemplateUnits.new(state[:entries],
|
|
41
|
+
failures: state[:failures], claimed_globs: claimed, root: root,
|
|
42
|
+
stats: state[:stats], plugin_signature: signature)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The entry point the LSP-facing {ProjectScan} is built through (#1038): the index a later run
|
|
46
|
+
# CARRIES, so it is compiled with no buffer binding — a unit compiled from an editor's in-flight bytes
|
|
47
|
+
# must never reach a snapshot someone reuses.
|
|
48
|
+
#
|
|
49
|
+
# Wrapped in {Cache::FileDigest.with_run} because ADR-87's racy guard is what makes the packs
|
|
50
|
+
# recorded here trustworthy: the recording instant has to be taken BEFORE the bytes are read, so a
|
|
51
|
+
# template written between the read and the stat is recorded as racy and always re-digested.
|
|
52
|
+
# `prepare_project_scan` is not itself inside a run scope (only `Runner#run` is), and with no active
|
|
53
|
+
# table `pack_stat` falls back to an instant taken AFTER its own `File.stat`, which can never be racy
|
|
54
|
+
# — a pack pairing the OLD digest with the NEW stat tuple, which every later publish would validate
|
|
55
|
+
# on the tuple fast path and serve stale. `strict:` rides along for the reason `Runner#run` carries
|
|
56
|
+
# it: under `cache.validation: digest` the stat tier is skipped, here as everywhere else.
|
|
57
|
+
def self.collect_for_scan(registry:, strict: false)
|
|
58
|
+
return TemplateUnits.empty if registry.nil?
|
|
59
|
+
|
|
60
|
+
Cache::FileDigest.with_run(strict: strict) { collect(registry: registry, buffer: nil) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The `(plugin, globs)` pairs of every plugin that claimed one, in registration order — which is what
|
|
64
|
+
# decides a contested path (see {.collect}).
|
|
65
|
+
#
|
|
66
|
+
# A plugin whose manifest cannot be read claims nothing. `Plugin::Base#manifest` raises for a class
|
|
67
|
+
# that declared none, and a registry is not guaranteed to hold only well-formed plugins (the loader
|
|
68
|
+
# reports such a failure through its own channel); refusing to glob is the quiet, correct answer here
|
|
69
|
+
# rather than a second report of the same defect.
|
|
70
|
+
def self.claims_for(registry)
|
|
71
|
+
registry.plugins.filter_map do |plugin|
|
|
72
|
+
globs = begin
|
|
73
|
+
plugin.manifest.template_globs
|
|
74
|
+
rescue StandardError
|
|
75
|
+
[]
|
|
76
|
+
end
|
|
77
|
+
next if globs.empty?
|
|
78
|
+
|
|
79
|
+
[plugin, globs]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
private_class_method :claims_for
|
|
83
|
+
|
|
84
|
+
# What a carried index is checked against, beyond the per-file freshness: the identity of the plugins
|
|
85
|
+
# that claimed a glob. A registry change is a whole-index rebuild, never a per-path question — the
|
|
86
|
+
# transform itself may have changed.
|
|
87
|
+
def self.plugin_signature(plugin)
|
|
88
|
+
"#{plugin.manifest.id}@#{plugin.manifest.version}"
|
|
89
|
+
rescue StandardError
|
|
90
|
+
plugin.class.to_s
|
|
91
|
+
end
|
|
92
|
+
private_class_method :plugin_signature
|
|
93
|
+
|
|
94
|
+
# The immutable half of one collection pass. Split off the mutable `state` hash so the per-plugin and
|
|
95
|
+
# per-path helpers stay inside the parameter-count budget.
|
|
96
|
+
Collection = Data.define(:root, :buffer, :carried)
|
|
97
|
+
private_constant :Collection
|
|
98
|
+
|
|
99
|
+
# #1047 — the carry is decided for a plugin's claim AS A WHOLE, not per path. A transform may read
|
|
100
|
+
# the plugin's OTHER claimed templates: rigor-actionpack seeds a partial's locals from the render
|
|
101
|
+
# sites in every view that renders it, so `users/_card`'s unit depends on the bytes of
|
|
102
|
+
# `users/show`. A per-path carry reused `_card`'s unit after `show` dropped the local it passed,
|
|
103
|
+
# and the stale seed survived every later publish. So if any of a plugin's claimed templates was
|
|
104
|
+
# edited, added or deleted since the carried index was built, none of its units is carried and the
|
|
105
|
+
# whole claim is recompiled. One change is not seen: a template that produced NO unit (declined, or
|
|
106
|
+
# raised) being deleted, because the carried rows record no owning plugin for it. A declined template
|
|
107
|
+
# contributed nothing a sibling could have read from its unit, which is why that is left as stated
|
|
108
|
+
# rather than tracked. What stays exempt is the editor's buffer — a keystroke is not a save,
|
|
109
|
+
# so #1038's per-keystroke property holds — and a path an earlier plugin already claimed.
|
|
110
|
+
#
|
|
111
|
+
# #1065 widened what "any of that plugin's claimed templates" covers: rigor-actionpack now claims the
|
|
112
|
+
# handlers it DECLINES, so editing one `.haml` costs the whole ERB claim its carry. Measured on
|
|
113
|
+
# mastodon at +4 ms — the templates are recompiled from the {RenderLocals} memo rather than from
|
|
114
|
+
# scratch — which is why the claim is decided as a whole here rather than split per handler.
|
|
115
|
+
def self.collect_plugin(plugin, globs, collection, state)
|
|
116
|
+
start_pass(plugin)
|
|
117
|
+
plan = expand(globs, collection.root, collection.buffer).map do |path|
|
|
118
|
+
physical = physical_path(path, collection.root, collection.buffer)
|
|
119
|
+
[path, physical, carried_entry(path, physical, collection, state)]
|
|
120
|
+
end
|
|
121
|
+
whole = whole_carry?(plugin, plan, collection, state)
|
|
122
|
+
plan.each do |path, physical, carried|
|
|
123
|
+
if carried && whole
|
|
124
|
+
state[:entries][path], state[:stats][path] = carried
|
|
125
|
+
else
|
|
126
|
+
compile(plugin, path, physical, state)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
private_class_method :collect_plugin
|
|
131
|
+
|
|
132
|
+
# {Plugin::Base#template_units_pass_started}, isolated: a plugin that raises there loses its own
|
|
133
|
+
# revalidation for this pass and nothing else.
|
|
134
|
+
def self.start_pass(plugin)
|
|
135
|
+
plugin.template_units_pass_started
|
|
136
|
+
rescue StandardError
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
private_class_method :start_pass
|
|
140
|
+
|
|
141
|
+
# True when nothing in this plugin's claim moved: every planned path is carried (or is exempt, see
|
|
142
|
+
# {.collect_plugin}), and no path the carried index held for this plugin has since vanished.
|
|
143
|
+
def self.whole_carry?(plugin, plan, collection, state)
|
|
144
|
+
return false if collection.carried.empty?
|
|
145
|
+
|
|
146
|
+
id = plugin.manifest.id
|
|
147
|
+
planned = plan.to_h { |path, _physical, _carried| [path, true] }
|
|
148
|
+
vanished = collection.carried.any? { |path, (entry, _packed)| entry&.plugin_id == id && !planned.key?(path) }
|
|
149
|
+
return false if vanished
|
|
150
|
+
|
|
151
|
+
plan.all? do |path, physical, carried|
|
|
152
|
+
carried || state[:entries].key?(path) || buffer_bound?(path, collection) ||
|
|
153
|
+
unchanged_without_unit?(path, physical, collection)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
private_class_method :whole_carry?
|
|
157
|
+
|
|
158
|
+
# A template the carried index READ and got no unit for — declined, or its transform raised — whose
|
|
159
|
+
# bytes have not moved. It is re-offered to the plugin as before, but it is not an edit.
|
|
160
|
+
def self.unchanged_without_unit?(path, physical, collection)
|
|
161
|
+
entry, packed = collection.carried[path]
|
|
162
|
+
entry.nil? && !packed.nil? && fresh?(physical, packed)
|
|
163
|
+
end
|
|
164
|
+
private_class_method :unchanged_without_unit?
|
|
165
|
+
|
|
166
|
+
# Reads one claimed template and hands its bytes to the plugin, recording the compiled unit and the
|
|
167
|
+
# freshness token a LATER run revalidates the carry against. A transform that raises costs this file
|
|
168
|
+
# its unit and nothing else.
|
|
169
|
+
def self.compile(plugin, path, physical, state)
|
|
170
|
+
id = plugin.manifest.id
|
|
171
|
+
source = read_source(physical, path, id, state[:failures])
|
|
172
|
+
return if source.nil?
|
|
173
|
+
|
|
174
|
+
state[:stats][path] = Cache::FileDigest.pack_stat(physical, Digest::SHA256.hexdigest(source))
|
|
175
|
+
units = transform(plugin, path, source, state)
|
|
176
|
+
return if units.nil?
|
|
177
|
+
|
|
178
|
+
fallback = "#{id}@#{plugin.manifest.version}"
|
|
179
|
+
units.each { |unit| record(state[:entries], unit, path, fallback, id, state[:failures]) }
|
|
180
|
+
end
|
|
181
|
+
private_class_method :compile
|
|
182
|
+
|
|
183
|
+
# The plugin call itself, with the ADR-2 isolation envelope around it: a raise is one `plugin_loader`
|
|
184
|
+
# row for this template and nil, which costs the file its unit and leaves the run intact. A transform
|
|
185
|
+
# that returns nothing is `[]` — an honest decline — not a failure.
|
|
186
|
+
def self.transform(plugin, path, source, state)
|
|
187
|
+
Array(plugin.template_units_for_file(path: path, source: source))
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
state[:failures] << TemplateUnits::Failure.new(plugin_id: plugin.manifest.id, path: path,
|
|
190
|
+
message: "#{e.class}: #{e.message}")
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
private_class_method :transform
|
|
194
|
+
|
|
195
|
+
# #1038 — the `[entry, stat_pack]` a warm index already compiled for `path`, when nothing that could
|
|
196
|
+
# change it has moved; nil means "compile it".
|
|
197
|
+
#
|
|
198
|
+
# Three per-path refusals, each load-bearing; the whole-index ones are {TemplateUnits#carry_over}'s.
|
|
199
|
+
# A path another plugin already claimed is left to the loop that follows, so a second claimant still
|
|
200
|
+
# runs and is still reported the way it was before this memo existed. A path the editor's buffer is
|
|
201
|
+
# bound to is ALWAYS recompiled, from the buffer's bytes: the saved file is not what the user is
|
|
202
|
+
# looking at, and a unit compiled from a buffer must never end up in an index a later run carries.
|
|
203
|
+
# Otherwise the file's own freshness decides, through the ADR-87 pack — a moved stat tuple falls back
|
|
204
|
+
# to the content digest, so a touched-but-unedited template is still a reuse and an edited one never
|
|
205
|
+
# is. Any stat failure (the template was deleted between the glob and here) reads as "not fresh",
|
|
206
|
+
# which sends the path down the ordinary read path and produces the ordinary failure row.
|
|
207
|
+
def self.carried_entry(path, physical, collection, state)
|
|
208
|
+
return nil if state[:entries].key?(path)
|
|
209
|
+
|
|
210
|
+
entry, packed = collection.carried[path]
|
|
211
|
+
return nil if entry.nil? || packed.nil?
|
|
212
|
+
return nil if buffer_bound?(path, collection)
|
|
213
|
+
return nil unless fresh?(physical, packed)
|
|
214
|
+
|
|
215
|
+
[entry, packed]
|
|
216
|
+
end
|
|
217
|
+
private_class_method :carried_entry
|
|
218
|
+
|
|
219
|
+
def self.buffer_bound?(path, collection)
|
|
220
|
+
buffer = collection.buffer
|
|
221
|
+
!buffer.nil? && TemplateUnitPaths.relative(buffer.logical_path, collection.root) == path
|
|
222
|
+
end
|
|
223
|
+
private_class_method :buffer_bound?
|
|
224
|
+
|
|
225
|
+
def self.fresh?(physical, packed)
|
|
226
|
+
Cache::FileDigest.stat_fresh?(physical, packed)
|
|
227
|
+
rescue StandardError
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
private_class_method :fresh?
|
|
231
|
+
|
|
232
|
+
# Editor mode (#146) — the in-flight buffer's bytes stand in for the file on disk, exactly as
|
|
233
|
+
# `Runner#parse_source` reads them for a `.rb` file. Without this a `--tmp-file` / `--instead-of` pair
|
|
234
|
+
# naming a TEMPLATE compiled the saved file and the editor got diagnostics for bytes it had already
|
|
235
|
+
# replaced. `BufferBinding#resolve` is deliberately NOT used: it compares the logical path by string,
|
|
236
|
+
# and a unit path is project-relative while the editor names its buffer absolutely.
|
|
237
|
+
def self.physical_path(path, root, buffer)
|
|
238
|
+
return buffer.physical_path if buffer && TemplateUnitPaths.relative(buffer.logical_path, root) == path
|
|
239
|
+
|
|
240
|
+
File.join(root, path)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.read_source(physical, path, plugin_id, failures)
|
|
244
|
+
File.binread(physical)
|
|
245
|
+
rescue StandardError => e
|
|
246
|
+
failures << TemplateUnits::Failure.new(plugin_id: plugin_id, path: path,
|
|
247
|
+
message: "could not be read (#{e.class}: #{e.message})")
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
private_class_method :read_source
|
|
251
|
+
|
|
252
|
+
# Sorted so the run's analysed-path order — and therefore the run cache key's `paths` slot — is
|
|
253
|
+
# independent of the filesystem's directory order.
|
|
254
|
+
#
|
|
255
|
+
# An editor buffer whose logical path MATCHES a claimed glob joins the set even when nothing is on
|
|
256
|
+
# disk at that path. That is the `didOpen` of a freshly created view: the file exists only in the
|
|
257
|
+
# editor, `Dir.glob` cannot see it, and without this the run fell through to parsing the tmp bytes as
|
|
258
|
+
# plain top-level Ruby — no declared `self`, no seeds, so a helper call read as
|
|
259
|
+
# `call.unresolved-toplevel` and the finding the editor was actually looking at was missed. The
|
|
260
|
+
# buffer's own bytes are what {.compile} then reads, and nothing else changes: an editor run is
|
|
261
|
+
# read-only-cached, and `Runner#template_unit_file_entries` already skips a path with no physical file.
|
|
262
|
+
def self.expand(globs, root, buffer = nil)
|
|
263
|
+
paths = globs.flat_map { |glob| Dir.glob(glob, base: root) }
|
|
264
|
+
.select { |path| File.file?(File.join(root, path)) }
|
|
265
|
+
buffered = buffer && TemplateUnitPaths.relative(buffer.logical_path, root)
|
|
266
|
+
paths |= [buffered] if buffered && TemplateUnitPaths.claims?(globs, buffered)
|
|
267
|
+
paths.uniq.sort
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private_class_method :expand
|
|
271
|
+
|
|
272
|
+
# A unit MUST name the file it was compiled from. Without the check a `path:` naming another project
|
|
273
|
+
# file silently REPLACED that file's source (the engine serves a unit's bytes for its own path), and a
|
|
274
|
+
# `path:` naming something outside the project root was analysed with no dependency-descriptor row —
|
|
275
|
+
# both from a plugin that only had to get one string wrong. A mismatch is reported, not dropped in
|
|
276
|
+
# silence, because a plugin author whose unit vanished has nothing to read.
|
|
277
|
+
def self.record(entries, unit, claimed_path, fallback, plugin_id, failures)
|
|
278
|
+
return unless unit.is_a?(Plugin::TemplateUnit)
|
|
279
|
+
|
|
280
|
+
unless unit.path == claimed_path
|
|
281
|
+
failures << TemplateUnits::Failure.new(
|
|
282
|
+
plugin_id: plugin_id, path: claimed_path,
|
|
283
|
+
message: "returned a unit for #{unit.path.inspect}, which is not the file it was offered; " \
|
|
284
|
+
"a unit may only name its own source"
|
|
285
|
+
)
|
|
286
|
+
return
|
|
287
|
+
end
|
|
288
|
+
# First claim wins — see {.collect}. A duplicate `logical_name` across two DIFFERENT paths is NOT
|
|
289
|
+
# refused: the two units are analysed separately and their summaries union under one `view:` key,
|
|
290
|
+
# which is the same reading a method reopened in two files gets.
|
|
291
|
+
return if entries.key?(unit.path)
|
|
292
|
+
|
|
293
|
+
entries[unit.path] = TemplateUnits::Entry.new(
|
|
294
|
+
logical_name: unit.logical_name, path: unit.path, source: unit.ruby_source,
|
|
295
|
+
line_map: unit.line_map, self_type: unit.self_type, locals: unit.locals,
|
|
296
|
+
ivar_seeds: unit.ivar_seeds, digest: unit.digest(fallback), unit_key: unit.unit_key,
|
|
297
|
+
plugin_id: plugin_id, suppressed_rules: unit.suppressed_rules
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
private_class_method :record
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# The path-spelling arithmetic behind {TemplateUnits} (#392).
|
|
6
|
+
#
|
|
7
|
+
# A template unit is keyed the way a claimed glob spells it — project-relative, as `Dir.glob(base:)`
|
|
8
|
+
# returns it. Every other spelling a path can arrive in has to reduce to that one before a lookup or a
|
|
9
|
+
# claim test, and there are more of them than there look to be: a language server names a buffer by its
|
|
10
|
+
# absolute path, a shell hands over `./app/views/x.rbx` or `lib/../app/views/x.rbx`, and the same
|
|
11
|
+
# directory reached through a symlink is the same directory (`Dir.pwd` is always resolved; on macOS an
|
|
12
|
+
# editor's `/var/…` and pwd's `/private/var/…` are one place). Each of those missed the unit and left
|
|
13
|
+
# the template parsed as plain Ruby, so the reduction lives in one place with the reasons attached.
|
|
14
|
+
module TemplateUnitPaths
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# A path as the globs spell it. An analysed path may arrive ABSOLUTE — the language server names a
|
|
18
|
+
# buffer by its full filesystem path, and `rigor check /abs/path` does too — while a claimed glob and
|
|
19
|
+
# everything `Dir.glob(base:)` returns are project-relative. Every lookup and every claim test goes
|
|
20
|
+
# through this, so the two spellings name one unit instead of silently missing each other (which is
|
|
21
|
+
# how an LSP publish for an open `.rbx` reported `call.unresolved-toplevel` for every helper while the
|
|
22
|
+
# unit sat in the index under its relative name). A path outside the root is left alone.
|
|
23
|
+
def relative(path, root)
|
|
24
|
+
# `expand_path` against the root first, so `./app/views/x.rbx`, `app/views/x.rbx` and
|
|
25
|
+
# `$ROOT/lib/../app/views/x.rbx` all reduce to the one absolute spelling before anything is
|
|
26
|
+
# compared. Without it an `--instead-of=./app/views/x.rbx` matched no unit and was parsed as plain
|
|
27
|
+
# Ruby — the spellings a shell and an editor produce are not the spelling `Dir.glob` returns.
|
|
28
|
+
root_dir = File.expand_path(root.to_s)
|
|
29
|
+
text = File.expand_path(path.to_s, root_dir)
|
|
30
|
+
# BOTH spellings of the root. `expand_path` does not resolve symlinks, and the root is not always
|
|
31
|
+
# already resolved: `Dir.pwd` is (which is why this was latent), but a caller may pass any path.
|
|
32
|
+
prefixes(root_dir).each do |prefix|
|
|
33
|
+
return text.delete_prefix(prefix) if text.start_with?(prefix)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The same directory reached through a symlink is the same directory. `Dir.pwd` is always the
|
|
37
|
+
# resolved form (`/private/var/…` on macOS) while an editor names a buffer by the path the user
|
|
38
|
+
# opened (`/var/…`), so a string compare alone loses the match.
|
|
39
|
+
resolved = resolved_path(text)
|
|
40
|
+
return text if resolved.nil?
|
|
41
|
+
|
|
42
|
+
prefixes(root_dir).each do |prefix|
|
|
43
|
+
return resolved.delete_prefix(prefix) if resolved.start_with?(prefix)
|
|
44
|
+
end
|
|
45
|
+
text
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def prefixes(root_dir)
|
|
49
|
+
[root_dir, (begin
|
|
50
|
+
File.realpath(root_dir)
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end)].compact.uniq.map { |dir| "#{dir}#{File::SEPARATOR}" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `text` with its **nearest existing ancestor** resolved and the rest re-joined. Realpathing the whole
|
|
57
|
+
# dirname is not enough: a buffer for a view in a directory the editor has not created yet
|
|
58
|
+
# (`app/views/reports/new.rbx` with no `reports/`) raises `ENOENT` there, and a nil answer meant a
|
|
59
|
+
# symlinked root lost the unit entirely for exactly the file most likely to be open.
|
|
60
|
+
def resolved_path(text)
|
|
61
|
+
remainder = []
|
|
62
|
+
current = text
|
|
63
|
+
loop do
|
|
64
|
+
parent = File.dirname(current)
|
|
65
|
+
remainder.unshift(File.basename(current))
|
|
66
|
+
real = begin
|
|
67
|
+
File.realpath(parent)
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
return File.join(real, *remainder) if real
|
|
72
|
+
return nil if parent == current
|
|
73
|
+
|
|
74
|
+
current = parent
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# `FNM_PATHNAME` so `*` does not cross a directory separator (the same reading `Dir.glob` gives the
|
|
79
|
+
# pattern), `FNM_EXTGLOB` so a `{html,text}` alternation in a claimed glob matches here as it did
|
|
80
|
+
# there — the two flags together are what make this predicate agree with the expansion above.
|
|
81
|
+
def claims?(globs, path)
|
|
82
|
+
# `relative` has already reduced an IN-root path to its project-relative form, so anything still
|
|
83
|
+
# absolute here is outside the project. An unanchored claim (`**/*.rbx`) would otherwise match it,
|
|
84
|
+
# and the plugin would be handed an absolute `path:` for a file the project does not contain.
|
|
85
|
+
return false if path.start_with?(File::SEPARATOR)
|
|
86
|
+
|
|
87
|
+
globs.any? { |glob| File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|