rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
|
|
3
5
|
require_relative "signature_path_audit"
|
|
4
|
-
|
|
6
|
+
# ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
|
|
7
|
+
# `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
|
|
8
|
+
# off the inference engine so the boot-slimming hit path stays engine-free.
|
|
9
|
+
require_relative "analysis/check_rules/rule_ids"
|
|
5
10
|
|
|
6
11
|
module Rigor
|
|
7
12
|
# Audits a loaded {Configuration} for the class of mistake where a configured value
|
|
@@ -36,12 +41,53 @@ module Rigor
|
|
|
36
41
|
# paths resolve against (the CLI's CWD), used only by the explicit-path checks.
|
|
37
42
|
# @return [Array<Warning>]
|
|
38
43
|
def self.warnings(configuration, project_root: Dir.pwd)
|
|
39
|
-
|
|
44
|
+
unknown_key_warnings(configuration) +
|
|
45
|
+
signature_path_warnings(configuration) +
|
|
40
46
|
library_warnings(configuration) +
|
|
41
47
|
rule_token_warnings(configuration) +
|
|
42
48
|
explicit_path_warnings(configuration, project_root)
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
# Top-level keys the loader does not own. The archetypal case is a typo — `excludee:` for
|
|
52
|
+
# `exclude:` — which the loader drops in silence, so the exclusion never applies and the run
|
|
53
|
+
# reports errors from the very files the user meant to skip, with nothing anywhere saying the key
|
|
54
|
+
# did nothing. That is exactly this module's class of mistake, and it was the one whole-key
|
|
55
|
+
# instance of it going unwarned.
|
|
56
|
+
#
|
|
57
|
+
# A reserved namespace ({Configuration::RESERVED_NAMESPACES}) is unknown *on purpose* — another
|
|
58
|
+
# implementation reads it from the same file — so it is exempt. {Configuration#unknown_keys}
|
|
59
|
+
# already applies that exemption; warning here would push users to delete the key their other
|
|
60
|
+
# tool needs.
|
|
61
|
+
#
|
|
62
|
+
# **Top level only, and deliberately.** A nested check would have to name each group's known keys,
|
|
63
|
+
# and DEFAULTS cannot supply them: `DEFAULTS["dependencies"]` carries `source_inference` and
|
|
64
|
+
# `budget_per_gem` but not `budget_overrun_strategy`, which is real (`Configuration::Dependencies`
|
|
65
|
+
# reads it), documented, and schema-declared — so a DEFAULTS-keyed nested check would flag a
|
|
66
|
+
# working config. `severity_overrides:` is an open map of rule ids besides. Nested unknown keys are
|
|
67
|
+
# the schema tier's job: every nested object in `schemas/rigor-config.schema.json` is
|
|
68
|
+
# `additionalProperties: false`, and `config_schema_spec` keeps it complete
|
|
69
|
+
# ([ADR-99](../../docs/adr/99-config-schema-authority.md), `docs/internal-spec/config.md`).
|
|
70
|
+
def self.unknown_key_warnings(configuration)
|
|
71
|
+
configuration.unknown_keys.map do |key|
|
|
72
|
+
suggestion = suggest_config_key(key)
|
|
73
|
+
hint = suggestion ? " Did you mean `#{suggestion}`?" : ""
|
|
74
|
+
Warning.new(
|
|
75
|
+
kind: :unknown_key,
|
|
76
|
+
message: "`#{key}` is not a recognized configuration key; it has no effect.#{hint}",
|
|
77
|
+
fields: { "key" => key, "suggestion" => suggestion }
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Nearest known key, or nil. Uses `DidYouMean::SpellChecker` — the engine behind Ruby's own
|
|
83
|
+
# `NoMethodError` hints, and the one `Plugin::Base.suggest` wraps. Called directly rather than
|
|
84
|
+
# through that helper because `plugin/base.rb` pulls Prism and the diagnostic model in, and
|
|
85
|
+
# ADR-87 WD4 keeps this file (loaded by every check) off that path; `did_you_mean` is stdlib.
|
|
86
|
+
def self.suggest_config_key(key)
|
|
87
|
+
DidYouMean::SpellChecker.new(dictionary: Configuration::KNOWN_KEYS).correct(key.to_s).first
|
|
88
|
+
end
|
|
89
|
+
private_class_method :suggest_config_key
|
|
90
|
+
|
|
45
91
|
# `signature_paths:` entries that resolve to nothing — delegated to {SignaturePathAudit},
|
|
46
92
|
# which mirrors the loader's `directory?` + recursive `**/*.rbs` acceptance test.
|
|
47
93
|
def self.signature_path_warnings(configuration)
|
|
@@ -37,11 +37,15 @@ module Rigor
|
|
|
37
37
|
"call.wrong-arity" => :error,
|
|
38
38
|
"call.argument-type-mismatch" => :warning,
|
|
39
39
|
"call.possible-nil-receiver" => :warning,
|
|
40
|
+
"call.raise-non-exception" => :warning,
|
|
40
41
|
"flow.always-raises" => :warning,
|
|
41
42
|
"flow.unreachable-branch" => :info,
|
|
42
43
|
"flow.dead-assignment" => :info,
|
|
43
44
|
"flow.always-truthy-condition" => :info,
|
|
44
45
|
"flow.unreachable-clause" => :info,
|
|
46
|
+
"flow.duplicate-hash-key" => :info,
|
|
47
|
+
"flow.return-in-ensure" => :info,
|
|
48
|
+
"flow.shadowed-rescue-clause" => :info,
|
|
45
49
|
"assert.type-mismatch" => :error,
|
|
46
50
|
"dump.type" => :info,
|
|
47
51
|
"def.return-type-mismatch" => :warning,
|
|
@@ -50,6 +54,16 @@ module Rigor
|
|
|
50
54
|
"def.override-return-widened" => :off,
|
|
51
55
|
"def.override-param-narrowed" => :off,
|
|
52
56
|
"def.ivar-write-mismatch" => :warning,
|
|
57
|
+
# ADR-8 companion (PHPStan IgnoreParseErrorRule-modelled): a broken suppression comment is
|
|
58
|
+
# equally bad in every profile — it silently fails to do what the author believes it does — so
|
|
59
|
+
# both rules stay :warning across all three profiles (including strict).
|
|
60
|
+
"suppression.unknown-rule" => :warning,
|
|
61
|
+
"suppression.empty" => :warning,
|
|
62
|
+
"suppression.unknown-marker" => :warning,
|
|
63
|
+
# ADR-100 — a new required diagnostic (ADR-50 WD1), so it is `:off` in every shipped profile and
|
|
64
|
+
# reaches a user only through the `use-of-void-value` bleeding-edge feature, which overrides this
|
|
65
|
+
# to `:warning`.
|
|
66
|
+
"static.value-use.void" => :off,
|
|
53
67
|
# Opt-in author assertion: you only see it if you wrote a
|
|
54
68
|
# `conforms-to` directive, so it stays a :warning even in
|
|
55
69
|
# lenient — it is never unsolicited noise.
|
|
@@ -62,6 +76,7 @@ module Rigor
|
|
|
62
76
|
"call.wrong-arity" => :error,
|
|
63
77
|
"call.argument-type-mismatch" => :error,
|
|
64
78
|
"call.possible-nil-receiver" => :error,
|
|
79
|
+
"call.raise-non-exception" => :error,
|
|
65
80
|
"flow.always-raises" => :error,
|
|
66
81
|
"flow.unreachable-branch" => :warning,
|
|
67
82
|
"flow.dead-assignment" => :warning,
|
|
@@ -71,6 +86,9 @@ module Rigor
|
|
|
71
86
|
# green; promote to :warning once Mastodon/GitLab/Redmine triage
|
|
72
87
|
# to zero net false positives.
|
|
73
88
|
"flow.unreachable-clause" => :info,
|
|
89
|
+
"flow.duplicate-hash-key" => :warning,
|
|
90
|
+
"flow.return-in-ensure" => :warning,
|
|
91
|
+
"flow.shadowed-rescue-clause" => :warning,
|
|
74
92
|
"assert.type-mismatch" => :error,
|
|
75
93
|
"dump.type" => :info,
|
|
76
94
|
"def.return-type-mismatch" => :warning,
|
|
@@ -79,6 +97,10 @@ module Rigor
|
|
|
79
97
|
"def.override-return-widened" => :warning,
|
|
80
98
|
"def.override-param-narrowed" => :warning,
|
|
81
99
|
"def.ivar-write-mismatch" => :warning,
|
|
100
|
+
"suppression.unknown-rule" => :warning,
|
|
101
|
+
"suppression.empty" => :warning,
|
|
102
|
+
"suppression.unknown-marker" => :warning,
|
|
103
|
+
"static.value-use.void" => :off,
|
|
82
104
|
"rbs_extended.unsatisfied-conformance" => :warning
|
|
83
105
|
}.freeze,
|
|
84
106
|
strict: {
|
|
@@ -88,6 +110,7 @@ module Rigor
|
|
|
88
110
|
"call.wrong-arity" => :error,
|
|
89
111
|
"call.argument-type-mismatch" => :error,
|
|
90
112
|
"call.possible-nil-receiver" => :error,
|
|
113
|
+
"call.raise-non-exception" => :error,
|
|
91
114
|
"flow.always-raises" => :error,
|
|
92
115
|
"flow.unreachable-branch" => :error,
|
|
93
116
|
"flow.dead-assignment" => :error,
|
|
@@ -96,6 +119,9 @@ module Rigor
|
|
|
96
119
|
# below its :error siblings) while it proves out — see the
|
|
97
120
|
# balanced-profile note above.
|
|
98
121
|
"flow.unreachable-clause" => :warning,
|
|
122
|
+
"flow.duplicate-hash-key" => :error,
|
|
123
|
+
"flow.return-in-ensure" => :error,
|
|
124
|
+
"flow.shadowed-rescue-clause" => :error,
|
|
99
125
|
"assert.type-mismatch" => :error,
|
|
100
126
|
"dump.type" => :error,
|
|
101
127
|
"def.return-type-mismatch" => :error,
|
|
@@ -104,6 +130,11 @@ module Rigor
|
|
|
104
130
|
"def.override-return-widened" => :error,
|
|
105
131
|
"def.override-param-narrowed" => :error,
|
|
106
132
|
"def.ivar-write-mismatch" => :error,
|
|
133
|
+
"suppression.unknown-rule" => :warning,
|
|
134
|
+
"suppression.empty" => :warning,
|
|
135
|
+
"suppression.unknown-marker" => :warning,
|
|
136
|
+
# `:off` even under strict: the gate is `bleeding_edge:`, not the profile (ADR-50 WD1 / ADR-100).
|
|
137
|
+
"static.value-use.void" => :off,
|
|
107
138
|
"rbs_extended.unsatisfied-conformance" => :error
|
|
108
139
|
}.freeze
|
|
109
140
|
}.freeze
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
require_relative "bleeding_edge"
|
|
6
|
+
require_relative "ci_detector"
|
|
6
7
|
require_relative "configuration/dependencies"
|
|
7
8
|
require_relative "configuration/severity_profile"
|
|
8
9
|
|
|
@@ -55,6 +56,13 @@ module Rigor
|
|
|
55
56
|
# filtering — the path must be named here (WD2 (b) of ADR-22).
|
|
56
57
|
"baseline" => nil,
|
|
57
58
|
"fold_platform_specific_paths" => false,
|
|
59
|
+
# ADR-67 WD6a — opt-in call-site parameter type inference on the `check` walk. When `true`, `rigor
|
|
60
|
+
# check` runs the {Inference::ParameterInferenceCollector} as a one-round pre-pass into the discovery
|
|
61
|
+
# seed, so an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved
|
|
62
|
+
# call-site argument types (precision-additive only — the in-body negative rules decline on the inferred
|
|
63
|
+
# lower bound, WD6b). `false` (the default, resolved off by every severity profile) keeps the table empty
|
|
64
|
+
# and the run byte-identical to today. Mutually exclusive with `--incremental` (WD6c).
|
|
65
|
+
"parameter_inference" => false,
|
|
58
66
|
"cache" => {
|
|
59
67
|
"path" => ".rigor/cache",
|
|
60
68
|
# LRU eviction cap in bytes (ADR-54 WD3). The least-recently-used entries are removed at the end of a
|
|
@@ -63,7 +71,18 @@ module Rigor
|
|
|
63
71
|
# would otherwise ever delete; a full active per-project set is ~2 MB, so the cap never touches live
|
|
64
72
|
# entries. Set explicitly to `null` to disable eviction (pre-WD3 behaviour: the cache grows until
|
|
65
73
|
# `--clear-cache`).
|
|
66
|
-
"max_bytes" => 268_435_456
|
|
74
|
+
"max_bytes" => 268_435_456,
|
|
75
|
+
# ADR-87 WD1 — file-freshness validation strategy. `"stat"` validates a recorded file
|
|
76
|
+
# dependency by stat-ing it first and re-hashing only when the `(size, mtime_ns, ctime_ns, inode)`
|
|
77
|
+
# tuple moved (or the racy window fires), so an unchanged monorepo hashes ~0 bytes on a warm run.
|
|
78
|
+
# `"digest"` restores the pre-ADR-87 behaviour of SHA-256'ing every recorded file on every run — the
|
|
79
|
+
# per-run escape hatch is the `RIGOR_STRICT_VALIDATION=1` env var, which wins over this setting.
|
|
80
|
+
# `"auto"` (default, #190) resolves per environment: `"digest"` when {CiDetector} recognises a CI
|
|
81
|
+
# provider — a fresh checkout regenerates every stat tuple, so the stat tier can never short-circuit
|
|
82
|
+
# there and stat-signature glob slots would recompute on every run — and `"stat"` everywhere else.
|
|
83
|
+
# A self-hosted runner with a persistent workspace opts back into the stat floor with an explicit
|
|
84
|
+
# `"stat"` (or `RIGOR_CI_DETECT=0`).
|
|
85
|
+
"validation" => "auto"
|
|
67
86
|
},
|
|
68
87
|
"plugins_io" => {
|
|
69
88
|
"network" => "disabled",
|
|
@@ -143,9 +162,45 @@ module Rigor
|
|
|
143
162
|
PATH_KEYS = %w[paths signature_paths pre_eval].freeze
|
|
144
163
|
private_constant :PATH_KEYS
|
|
145
164
|
|
|
165
|
+
# Top-level keys this implementation DECLARES but never reads — see `docs/internal-spec/config.md`
|
|
166
|
+
# § "Reserved namespaces" and ADR-99.
|
|
167
|
+
#
|
|
168
|
+
# A sibling implementation (`rigor-rs`, which vendors our schema rather than keeping its own)
|
|
169
|
+
# groups keys for concepts we do not have under its own namespace, so one `.rigor.yml` feeds both.
|
|
170
|
+
# Such a key answers to the SCHEMA TIER ONLY: it is type-checked where it is written, and here it
|
|
171
|
+
# is never read, never validated, never coerced, and never an error however invalid its value.
|
|
172
|
+
#
|
|
173
|
+
# That is already the emergent behaviour — `#initialize` fetches each key it owns and never
|
|
174
|
+
# enumerates the rest — so this constant does not gate the runtime. It exists to make the
|
|
175
|
+
# reservation FINDABLE by the next person adding config validation (#166 is exactly that), and to
|
|
176
|
+
# give the schema-parity spec something to key on: a reserved namespace is by definition absent
|
|
177
|
+
# from DEFAULTS, so the DEFAULTS-driven schema gate can never see it.
|
|
178
|
+
RESERVED_NAMESPACES = %w[rigor_rs].freeze
|
|
179
|
+
|
|
180
|
+
# Every top-level key a conforming `.rigor.yml` may carry: the keys this implementation owns, plus
|
|
181
|
+
# `includes:`, plus the namespaces reserved for another implementation. Anything else is recorded in
|
|
182
|
+
# {#unknown_keys} and warned about by {ConfigAudit}, and is also the did-you-mean dictionary for a
|
|
183
|
+
# near-miss.
|
|
184
|
+
#
|
|
185
|
+
# `includes:` is a load-time directive — {load_with_includes} `delete`s it while merging, so it never
|
|
186
|
+
# reaches `#initialize` and could not be seen as unknown regardless. It is listed because a user
|
|
187
|
+
# legitimately writes it, so a typo like `include:` must be able to suggest it.
|
|
188
|
+
KNOWN_KEYS = (DEFAULTS.keys + %w[includes] + RESERVED_NAMESPACES).freeze
|
|
189
|
+
|
|
190
|
+
# Top-level keys the loaded config carried that this implementation does not own — neither a
|
|
191
|
+
# {DEFAULTS} key, nor `includes:`, nor a reserved namespace. Recorded rather than acted on: an
|
|
192
|
+
# unknown key stays as inert at run time as it has always been, and {ConfigAudit} turns the record
|
|
193
|
+
# into a warning. Empty for every conforming config.
|
|
194
|
+
#
|
|
195
|
+
# This exists because `#initialize` fetches each key it owns and never enumerates the rest, so by
|
|
196
|
+
# the time anything holds a Configuration the unknown keys are gone. The audit reads a
|
|
197
|
+
# Configuration, so without this the class of mistake it exists to catch — a value that silently
|
|
198
|
+
# resolves to nothing — was structurally invisible to it for whole keys.
|
|
199
|
+
attr_reader :unknown_keys
|
|
200
|
+
|
|
146
201
|
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
|
|
147
|
-
:disabled_rules,
|
|
148
|
-
:libraries, :signature_paths, :fold_platform_specific_paths,
|
|
202
|
+
:cache_validation, :disabled_rules,
|
|
203
|
+
:libraries, :signature_paths, :fold_platform_specific_paths, :parameter_inference,
|
|
149
204
|
:plugins_io_network, :plugins_io_allowed_paths,
|
|
150
205
|
:plugins_io_allowed_url_hosts,
|
|
151
206
|
:severity_profile, :severity_overrides,
|
|
@@ -168,10 +223,67 @@ module Rigor
|
|
|
168
223
|
# [PHPStan](https://phpstan.org/config-reference#paths).
|
|
169
224
|
def self.load(path = nil)
|
|
170
225
|
resolved = path || discover
|
|
171
|
-
|
|
226
|
+
data =
|
|
227
|
+
if resolved.nil? || !File.exist?(resolved)
|
|
228
|
+
DEFAULTS
|
|
229
|
+
else
|
|
230
|
+
DEFAULTS.merge(load_with_includes(resolved))
|
|
231
|
+
end
|
|
232
|
+
new(autowire_default_plugins(data))
|
|
233
|
+
end
|
|
172
234
|
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
# ADR-93 WD2 — the one bundled plugin default-wired without a `plugins:` entry. `rigor-rbs-inline` is the
|
|
236
|
+
# gem; `rbs-inline` is its manifest id (the loader raises on a duplicate id, so both forms count as
|
|
237
|
+
# "already listed").
|
|
238
|
+
AUTOWIRED_RBS_INLINE_GEM = "rigor-rbs-inline"
|
|
239
|
+
AUTOWIRED_RBS_INLINE_ID = "rbs-inline"
|
|
240
|
+
|
|
241
|
+
# ADR-93 WD2 — default-wire the bundled `rigor-rbs-inline` plugin, in WD1's annotation-gated,
|
|
242
|
+
# magic-comment-free mode, when the upstream `rbs-inline` library is resolvable and the user has not
|
|
243
|
+
# already listed the plugin. Runs from {load} only — the real-project route — never from a bare
|
|
244
|
+
# {Configuration.new}, so the suite's unit constructions do not auto-wire. The gate is presence-based
|
|
245
|
+
# (ADR-72's shape): an annotation-free project pays only a comment scan and contributes nothing, so
|
|
246
|
+
# wiring it on cannot regress the run. This partially reverses the ADR-27/ADR-31 auto-load deferral for
|
|
247
|
+
# this one bundled plugin (the executed code is already vendored, not arbitrary third-party plugin code).
|
|
248
|
+
# Opt out with a `plugins:` entry disabling it (`enabled: false`), or per-file `# rbs_inline: disabled`.
|
|
249
|
+
def self.autowire_default_plugins(data)
|
|
250
|
+
entries = Array(data["plugins"])
|
|
251
|
+
return data if entries.any? { |entry| rbs_inline_plugin_entry?(entry) }
|
|
252
|
+
return data unless rbs_inline_library_resolvable?
|
|
253
|
+
|
|
254
|
+
merged = data.dup
|
|
255
|
+
merged["plugins"] = entries + [{
|
|
256
|
+
"gem" => AUTOWIRED_RBS_INLINE_GEM,
|
|
257
|
+
"id" => AUTOWIRED_RBS_INLINE_ID,
|
|
258
|
+
"config" => { "require_magic_comment" => false }
|
|
259
|
+
}]
|
|
260
|
+
merged
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# True when a `plugins:` entry (String or Hash, string- or symbol-keyed) already references the
|
|
264
|
+
# `rigor-rbs-inline` plugin by gem name or by manifest id. Single-homed here so the CLI's
|
|
265
|
+
# `--treat-all-as-inline-rbs` injection and the WD2 auto-wire gate agree on what "already listed" means.
|
|
266
|
+
def self.rbs_inline_plugin_entry?(entry)
|
|
267
|
+
case entry
|
|
268
|
+
when String
|
|
269
|
+
entry == AUTOWIRED_RBS_INLINE_GEM
|
|
270
|
+
when Hash
|
|
271
|
+
string_keyed = entry.to_h { |k, v| [k.to_s, v] }
|
|
272
|
+
string_keyed["gem"] == AUTOWIRED_RBS_INLINE_GEM || string_keyed["id"] == AUTOWIRED_RBS_INLINE_ID
|
|
273
|
+
else
|
|
274
|
+
false
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# A probe with no load side effect: it does not `require` the library, only asks whether RubyGems can
|
|
279
|
+
# resolve the name. `Gem::Specification.find_by_name` raises `Gem::MissingSpecError` (a `Gem::LoadError`)
|
|
280
|
+
# when the gem is absent — the standalone `gem install rigortype` case, WD3's residual. ADR-90's
|
|
281
|
+
# project-bundle fallback resolves the same name at the plugin's own `require` time.
|
|
282
|
+
def self.rbs_inline_library_resolvable?
|
|
283
|
+
Gem::Specification.find_by_name("rbs-inline")
|
|
284
|
+
true
|
|
285
|
+
rescue Gem::LoadError
|
|
286
|
+
false
|
|
175
287
|
end
|
|
176
288
|
|
|
177
289
|
# Returns the path to the config file Rigor would load under auto-discovery, or `nil` when neither
|
|
@@ -280,6 +392,10 @@ module Rigor
|
|
|
280
392
|
|
|
281
393
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
282
394
|
def initialize(data = DEFAULTS)
|
|
395
|
+
# Record before the per-key fetches below discard the evidence. Top level only, deliberately —
|
|
396
|
+
# see {ConfigAudit.unknown_key_warnings} for why a nested check cannot key on DEFAULTS.
|
|
397
|
+
@unknown_keys = (data.keys.map(&:to_s) - KNOWN_KEYS).sort.freeze
|
|
398
|
+
|
|
283
399
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
284
400
|
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
285
401
|
|
|
@@ -300,10 +416,14 @@ module Rigor
|
|
|
300
416
|
@baseline_path = coerce_baseline_path(data.fetch("baseline", DEFAULTS.fetch("baseline")))
|
|
301
417
|
@fold_platform_specific_paths = data.fetch(
|
|
302
418
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
303
|
-
)
|
|
419
|
+
)
|
|
420
|
+
# ADR-67 WD6a — resolve to a strict Boolean so a truthy non-`true` value (e.g. a stray String) does not
|
|
421
|
+
# silently enable the gate; only the literal `true` activates the check-walk collector pre-pass.
|
|
422
|
+
@parameter_inference = data.fetch("parameter_inference", DEFAULTS.fetch("parameter_inference")) == true
|
|
304
423
|
@cache_path = cache.fetch("path").to_s
|
|
305
424
|
raw_max = cache.fetch("max_bytes")
|
|
306
425
|
@cache_max_bytes = raw_max.nil? ? nil : Integer(raw_max)
|
|
426
|
+
@cache_validation = coerce_cache_validation(cache.fetch("validation", "auto"))
|
|
307
427
|
@plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
|
|
308
428
|
@plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
|
|
309
429
|
@plugins_io_allowed_url_hosts = Array(plugins_io.fetch("allowed_url_hosts")).map(&:to_s).freeze
|
|
@@ -340,6 +460,20 @@ module Rigor
|
|
|
340
460
|
end
|
|
341
461
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
342
462
|
|
|
463
|
+
# Resolves the `cache.validation` tri-state to the boolean the run installs via
|
|
464
|
+
# {Cache::FileDigest.with_run}'s `strict:`. Explicit `"digest"` / `"stat"` win outright; the `"auto"`
|
|
465
|
+
# default is strict exactly when {CiDetector} recognises a CI provider (#190) — deliberately NOT stored at
|
|
466
|
+
# construction so the resolution honours the environment of the run, not of `Configuration.load`, and the
|
|
467
|
+
# frozen carrier stays env-independent. `RIGOR_STRICT_VALIDATION=1` is enforced separately inside
|
|
468
|
+
# {Cache::FileDigest.strict_validation?} and wins over all three values.
|
|
469
|
+
def cache_validation_strict?(env = ENV)
|
|
470
|
+
case cache_validation
|
|
471
|
+
when "digest" then true
|
|
472
|
+
when "stat" then false
|
|
473
|
+
else !CiDetector.detect(env).nil?
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
343
477
|
def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
344
478
|
{
|
|
345
479
|
"target_ruby" => target_ruby,
|
|
@@ -351,9 +485,11 @@ module Rigor
|
|
|
351
485
|
"signature_paths" => signature_paths,
|
|
352
486
|
"pre_eval" => pre_eval,
|
|
353
487
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
488
|
+
"parameter_inference" => parameter_inference,
|
|
354
489
|
"cache" => {
|
|
355
490
|
"path" => cache_path,
|
|
356
|
-
"max_bytes" => cache_max_bytes
|
|
491
|
+
"max_bytes" => cache_max_bytes,
|
|
492
|
+
"validation" => cache_validation
|
|
357
493
|
},
|
|
358
494
|
"plugins_io" => {
|
|
359
495
|
"network" => plugins_io_network.to_s,
|
|
@@ -483,6 +619,18 @@ module Rigor
|
|
|
483
619
|
value.to_s
|
|
484
620
|
end
|
|
485
621
|
|
|
622
|
+
# ADR-87 WD1 — `cache.validation` is `"auto"` (default), `"stat"`, or `"digest"`. An unrecognised value
|
|
623
|
+
# fails soft to the default rather than aborting a check over a typo — the strict `"digest"` behaviour is
|
|
624
|
+
# the safe fallback either way, and the always-available `RIGOR_STRICT_VALIDATION=1` env escape hatch does
|
|
625
|
+
# not depend on this value being valid.
|
|
626
|
+
VALID_CACHE_VALIDATIONS = %w[auto stat digest].freeze
|
|
627
|
+
private_constant :VALID_CACHE_VALIDATIONS
|
|
628
|
+
|
|
629
|
+
def coerce_cache_validation(value)
|
|
630
|
+
str = value.to_s
|
|
631
|
+
VALID_CACHE_VALIDATIONS.include?(str) ? str : "auto"
|
|
632
|
+
end
|
|
633
|
+
|
|
486
634
|
def coerce_network_policy(value)
|
|
487
635
|
sym = value.to_sym
|
|
488
636
|
unless VALID_NETWORK_POLICIES.include?(sym)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# Slice A stdlib expansion. Stdlib libraries that `Environment.for_project` loads on top of RBS core
|
|
6
|
+
# unless the caller passes an explicit `libraries:` array. Each entry MUST be a stdlib library name
|
|
7
|
+
# accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft
|
|
8
|
+
# (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common
|
|
9
|
+
# stdlib surface a Ruby program is likely to import, plus the analyzer-adjacent gems shipping their own
|
|
10
|
+
# RBS in this bundle (`prism`, `rbs`). The list below is the enumeration — no prose gloss restates a
|
|
11
|
+
# subset of it, here or in `docs/internal-spec/inference-engine.md` § Environment Surface, because a
|
|
12
|
+
# restated subset silently rots as entries are added. On hosts where one of these libraries is not
|
|
13
|
+
# installed, the loader silently drops it.
|
|
14
|
+
#
|
|
15
|
+
# Callers MAY add to the default by passing `libraries: %w[csv ...]`; the explicit list is appended to
|
|
16
|
+
# `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an
|
|
17
|
+
# `RbsLoader` directly instead of going through `for_project`.
|
|
18
|
+
#
|
|
19
|
+
# ADR-87 WD4 — extracted to its own light file (no engine requires) so the boot-slimming hit probe can
|
|
20
|
+
# reconstruct `Environment.for_project`'s merged library list — which feeds the run cache KEY's
|
|
21
|
+
# `rbs.libraries` slot — WITHOUT building the RBS environment (or loading the inference engine).
|
|
22
|
+
DEFAULT_LIBRARIES = %w[
|
|
23
|
+
pathname optparse json yaml fileutils tempfile tmpdir
|
|
24
|
+
stringio forwardable digest securerandom
|
|
25
|
+
uri logger date
|
|
26
|
+
pp delegate observable abbrev find tsort singleton
|
|
27
|
+
shellwords benchmark base64 did_you_mean
|
|
28
|
+
monitor mutex_m timeout
|
|
29
|
+
open3 erb etc ipaddr bigdecimal bigdecimal-math
|
|
30
|
+
prettyprint random-formatter time open-uri resolv
|
|
31
|
+
csv pstore objspace io-console cgi cgi-escape
|
|
32
|
+
strscan
|
|
33
|
+
prism rbs
|
|
34
|
+
].freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -90,10 +90,48 @@ module Rigor
|
|
|
90
90
|
add_project_signatures(env, signature_paths)
|
|
91
91
|
add_virtual_rbs(env, virtual_rbs)
|
|
92
92
|
synthesize_missing_namespaces(env)
|
|
93
|
-
resolved = env
|
|
93
|
+
env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
94
94
|
stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# True when `content` parses as an RBS signature. {#virtual_rbs_collision_quarantined} uses this to
|
|
98
|
+
# tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
|
|
99
|
+
# (the synthesizer's own WD6 skip, reported separately).
|
|
100
|
+
def parseable_rbs?(content)
|
|
101
|
+
::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor: virtual parse check)", content: content))
|
|
102
|
+
true
|
|
103
|
+
rescue ::RBS::BaseError
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Backstop for a virtual-vs-anything `RBS::DuplicatedDeclarationError` that only materialises at
|
|
108
|
+
# `resolve_type_names` (which rebuilds the env from `sources`). {.add_virtual_rbs}'s transactional
|
|
109
|
+
# rescue already handles the add-time case — empirically everything on rbs 4.x — but the rbs gemspec
|
|
110
|
+
# range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an rbs-internal choice
|
|
111
|
+
# this code must not depend on. Resolution rule is the same as the add-time path: the explicit
|
|
112
|
+
# signature wins, the colliding VIRTUAL buffer is dropped whole (`RBS::Environment#unload`) and
|
|
113
|
+
# resolution retries; every pass removes at least one virtual buffer, so the loop is bounded by the
|
|
114
|
+
# virtual-entry count. A duplicate involving no virtual buffer (sig-vs-sig), or an env without
|
|
115
|
+
# `#unload` (rbs 3.x), re-raises into the existing one-warning degrade path.
|
|
116
|
+
#
|
|
117
|
+
# The dropped set is not returned: consumers recover it from the built env via
|
|
118
|
+
# {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
|
|
119
|
+
def resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
120
|
+
virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
|
|
121
|
+
(virtual_names.size + 1).times do
|
|
122
|
+
return [env, env.resolve_type_names]
|
|
123
|
+
rescue ::RBS::DuplicatedDeclarationError => e
|
|
124
|
+
raise unless env.respond_to?(:unload)
|
|
125
|
+
|
|
126
|
+
culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name }
|
|
127
|
+
.uniq.select { |name| virtual_names.include?(name) }
|
|
128
|
+
raise if culprits.empty?
|
|
129
|
+
|
|
130
|
+
env = env.unload(culprits)
|
|
131
|
+
end
|
|
132
|
+
[env, env.resolve_type_names]
|
|
133
|
+
end
|
|
134
|
+
|
|
97
135
|
# ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
|
|
98
136
|
# signature declares — `def x: () -> DRb::DRbServer` when the `drb` RBS is not available, or a stale
|
|
99
137
|
# reference to its own removed `Textbringer::EditorError` — makes
|
|
@@ -356,9 +394,20 @@ module Rigor
|
|
|
356
394
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
357
395
|
add_parsed_decls(env, buffer, directives, decls)
|
|
358
396
|
rescue ::RBS::BaseError
|
|
359
|
-
# WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down
|
|
360
|
-
#
|
|
361
|
-
#
|
|
397
|
+
# WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for
|
|
398
|
+
# a parse error, skipping the entry is enough. But `RBS::Environment#add_source` appends to
|
|
399
|
+
# `env.sources` BEFORE inserting decls, so when the raise is a mid-insert
|
|
400
|
+
# `RBS::DuplicatedDeclarationError` (the entry declares a constant the project's own `sig/`
|
|
401
|
+
# already declares — the expected state for a project migrating between `sig/` and inline
|
|
402
|
+
# annotations, not an authoring error), the POISONED SOURCE is left behind, and
|
|
403
|
+
# `resolve_type_names` — which rebuilds the env from `sources` — re-raises the same error outside
|
|
404
|
+
# this rescue and collapses the WHOLE env to nil (measured on herb: 1,490 classes → 0, `require`
|
|
405
|
+
# itself stopped resolving, 74 false `call.unresolved-toplevel`). Make the skip transactional:
|
|
406
|
+
# drop the poisoned source. The explicit `.rbs` declaration wins — the spec keeps standalone
|
|
407
|
+
# `.rbs` files "the preferred place for complete type definitions" (`overview.md`) — and
|
|
408
|
+
# {#warn_about_virtual_rbs_collisions} names the dropped file. `sources` is the rbs 4.x shape;
|
|
409
|
+
# under the 3.x API this degrades to today's behaviour.
|
|
410
|
+
env.sources.reject! { |source| source.buffer.name == buffer.name } if env.respond_to?(:sources)
|
|
362
411
|
end
|
|
363
412
|
end
|
|
364
413
|
|
|
@@ -478,6 +527,55 @@ module Rigor
|
|
|
478
527
|
names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
|
|
479
528
|
end
|
|
480
529
|
|
|
530
|
+
# The project `signature_paths:` files that were QUARANTINED this run (they do not parse, so
|
|
531
|
+
# {RbsLoader.add_project_signatures} skipped them to keep the rest of the env alive), as
|
|
532
|
+
# `[absolute_path, first_error_line]` pairs. Memoised per loader: the detection re-parses only the user's
|
|
533
|
+
# own `sig/` set, but every consumer (the `rbs.coverage.quarantined-signature` diagnostic, `rigor doctor`,
|
|
534
|
+
# the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
|
|
535
|
+
# quarantined, so the condition is invisible in the cached env itself.
|
|
536
|
+
#
|
|
537
|
+
# @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
|
|
538
|
+
def quarantined_signatures
|
|
539
|
+
@state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# The total RBS-environment build failure captured this run, or nil when the env built. Unlike
|
|
543
|
+
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived by
|
|
544
|
+
# re-parsing so a cache HIT reports it too — a total failure (typically `RBS::DuplicatedDeclarationError`:
|
|
545
|
+
# a `signature_paths:` entry redeclaring a constant/class Rigor's bundled RBS already ships) collapses the
|
|
546
|
+
# WHOLE env to nil. A failed build produces no cached success to hide behind (nothing is persisted, so
|
|
547
|
+
# every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue rather than
|
|
548
|
+
# re-derived. Forcing `env` (any query does) populates it.
|
|
549
|
+
#
|
|
550
|
+
# @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
|
|
551
|
+
# conflicting_buffer_names]`, or nil when the environment built successfully.
|
|
552
|
+
def env_build_failure
|
|
553
|
+
env unless @state[:env_loaded]
|
|
554
|
+
@state[:env_build_failure]
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Virtual (inline-synthesized) contributions dropped by the collision quarantine
|
|
558
|
+
# ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
|
|
559
|
+
# entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
|
|
560
|
+
# through the synthesis reporter instead). Derived from the env rather than recorded during build —
|
|
561
|
+
# the {#quarantined_signatures} trick — so a cache HIT, which never runs the build, reports the same
|
|
562
|
+
# condition: the marshalled env simply lacks the dropped buffers.
|
|
563
|
+
#
|
|
564
|
+
# @return [Array<String>] virtual buffer names (source-file paths) whose contribution was dropped.
|
|
565
|
+
def virtual_rbs_collision_quarantined
|
|
566
|
+
@state[:virtual_rbs_collisions] ||= begin
|
|
567
|
+
built = @state[:env]
|
|
568
|
+
if built.nil? || @virtual_rbs.empty?
|
|
569
|
+
[].freeze
|
|
570
|
+
else
|
|
571
|
+
present = built.buffers.to_set(&:name)
|
|
572
|
+
@virtual_rbs.filter_map do |name, content|
|
|
573
|
+
name if !content.empty? && !present.include?(name) && self.class.parseable_rbs?(content)
|
|
574
|
+
end.freeze
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
481
579
|
# The referenced-but-undeclared types {.stub_missing_referenced_types} stubbed so the project classes
|
|
482
580
|
# that mention them could build (e.g. an unavailable `DRb::DRbServer`, or a stale
|
|
483
581
|
# `Textbringer::EditorError`). Recovered off the built env like {#synthesized_namespaces}, so it
|
|
@@ -970,8 +1068,10 @@ module Rigor
|
|
|
970
1068
|
@state[:env_loaded] = true
|
|
971
1069
|
@state[:env] = cache_store ? cached_env : build_env
|
|
972
1070
|
warn_about_quarantined_signatures
|
|
1071
|
+
warn_about_virtual_rbs_collisions
|
|
973
1072
|
@state[:env]
|
|
974
1073
|
rescue ::RBS::BaseError => e
|
|
1074
|
+
record_env_build_failure(e)
|
|
975
1075
|
warn_about_env_build_failure_once(e)
|
|
976
1076
|
@state[:env] = nil
|
|
977
1077
|
end
|
|
@@ -982,10 +1082,15 @@ module Rigor
|
|
|
982
1082
|
# is the visibility half of the fix: a shrinking diagnostic count must never be mistaken for a clean run
|
|
983
1083
|
# when it actually means "your sig/ stopped loading". No-op when `signature_paths:` is empty (the cost is
|
|
984
1084
|
# then a single empty-set check) or every file parses.
|
|
1085
|
+
#
|
|
1086
|
+
# `rigor check` ALSO reports this as the `rbs.coverage.quarantined-signature` diagnostic, which is what
|
|
1087
|
+
# reaches JSON / SARIF / CI annotations / the LSP. The banner is kept because the commands that build an
|
|
1088
|
+
# env WITHOUT producing a diagnostic stream (`coverage`, `sig-gen`) have no other channel, and a silently
|
|
1089
|
+
# degraded env is exactly what misleads there too.
|
|
985
1090
|
def warn_about_quarantined_signatures
|
|
986
1091
|
return if @state[:quarantine_warned]
|
|
987
1092
|
|
|
988
|
-
quarantined =
|
|
1093
|
+
quarantined = quarantined_signatures
|
|
989
1094
|
return if quarantined.empty?
|
|
990
1095
|
|
|
991
1096
|
@state[:quarantine_warned] = true
|
|
@@ -1002,6 +1107,60 @@ module Rigor
|
|
|
1002
1107
|
)
|
|
1003
1108
|
end
|
|
1004
1109
|
|
|
1110
|
+
# The collision twin of {#warn_about_quarantined_signatures}: name, once per run, the source files
|
|
1111
|
+
# whose inline-synthesized RBS was dropped because it collides with a declaration another signature
|
|
1112
|
+
# source already made ({.resolve_quarantining_virtual_collisions} — the explicit `.rbs` wins). Without
|
|
1113
|
+
# this the drop is silent, and "my `#:` annotation does nothing" has no visible cause. Reads the
|
|
1114
|
+
# derived {#virtual_rbs_collision_quarantined}, so a cache HIT warns identically.
|
|
1115
|
+
def warn_about_virtual_rbs_collisions
|
|
1116
|
+
return if @state[:virtual_collision_warned]
|
|
1117
|
+
|
|
1118
|
+
dropped = virtual_rbs_collision_quarantined
|
|
1119
|
+
return if dropped.empty?
|
|
1120
|
+
|
|
1121
|
+
@state[:virtual_collision_warned] = true
|
|
1122
|
+
listed = dropped.first(QUARANTINE_WARN_LIMIT)
|
|
1123
|
+
more = dropped.size - listed.size
|
|
1124
|
+
lines = listed.map { |name| " - #{name}" }
|
|
1125
|
+
lines << " … and #{more} more" if more.positive?
|
|
1126
|
+
warn(
|
|
1127
|
+
"rigor: dropped inline-RBS contribution(s) from #{dropped.size} file(s): they declare a
|
|
1128
|
+
" \
|
|
1129
|
+
"constant, alias, or global that another signature source (typically the project's own
|
|
1130
|
+
" \
|
|
1131
|
+
"`sig/`) already declares. The explicit `.rbs` declaration wins, so inline annotations in
|
|
1132
|
+
" \
|
|
1133
|
+
"these files do not bind. Remove the duplication from either side to restore them:
|
|
1134
|
+
" \
|
|
1135
|
+
"#{lines.join("\n")}"
|
|
1136
|
+
)
|
|
1137
|
+
end
|
|
1138
|
+
|
|
1139
|
+
# Records the total RBS-environment build failure captured in {#env}'s rescue so the analysis layer can
|
|
1140
|
+
# surface it as the `rbs.coverage.environment-build-failed` diagnostic (the twin of
|
|
1141
|
+
# {#quarantined_signatures}: quarantine keeps the env alive minus one file, a total failure collapses the
|
|
1142
|
+
# WHOLE env to nil, so every type-of query degrades to `Dynamic[top]` and most rules stop firing). Stored
|
|
1143
|
+
# as `[error_class_name, first_error_line, conflicting_buffer_names]`. The buffer names are lifted off a
|
|
1144
|
+
# `RBS::DuplicatedDeclarationError#decls` — the typical failure, a `signature_paths:` entry redeclaring a
|
|
1145
|
+
# constant/class Rigor's bundled RBS already ships — so the diagnostic can name the colliding files.
|
|
1146
|
+
def record_env_build_failure(error)
|
|
1147
|
+
first_line = error.message.to_s.lines.first.to_s.strip
|
|
1148
|
+
@state[:env_build_failure] = [error.class.name, first_line, env_build_conflict_buffers(error)].freeze
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# The buffer (file) names carried by the colliding declarations of a `RBS::DuplicatedDeclarationError`, so
|
|
1152
|
+
# the diagnostic names the conflicting signature files rather than guessing. A buffer name is a `String`
|
|
1153
|
+
# path for a project `signature_paths:` file and a `Pathname` for a bundled RBS file, so each is coerced
|
|
1154
|
+
# to `String`. Other RBS build errors carry no `.decls`; they yield an empty list and the diagnostic
|
|
1155
|
+
# falls back to the message's first line alone.
|
|
1156
|
+
def env_build_conflict_buffers(error)
|
|
1157
|
+
return [].freeze unless error.respond_to?(:decls)
|
|
1158
|
+
|
|
1159
|
+
error.decls.filter_map { |decl| decl.location&.buffer&.name }.map(&:to_s).uniq.freeze
|
|
1160
|
+
rescue ::RBS::BaseError, StandardError
|
|
1161
|
+
[].freeze
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1005
1164
|
def warn_about_env_build_failure_once(error)
|
|
1006
1165
|
return if @state[:env_build_warned]
|
|
1007
1166
|
|