rigortype 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- 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 +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- 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 +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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 +191 -0
- 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/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- 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/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -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,35 @@
|
|
|
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 (`pathname`, `optparse`, `json`, `yaml`, `fileutils`,
|
|
10
|
+
# `tempfile`, `uri`, `logger`, `date`) plus the analyzer-adjacent gems shipping their own RBS in this
|
|
11
|
+
# bundle (`prism`, `rbs`). On hosts where one of these libraries is not installed, the loader silently
|
|
12
|
+
# drops it.
|
|
13
|
+
#
|
|
14
|
+
# Callers MAY add to the default by passing `libraries: %w[csv ...]`; the explicit list is appended to
|
|
15
|
+
# `DEFAULT_LIBRARIES` and de-duplicated. Callers that need a strictly RBS-core view MUST construct an
|
|
16
|
+
# `RbsLoader` directly instead of going through `for_project`.
|
|
17
|
+
#
|
|
18
|
+
# ADR-87 WD4 — extracted to its own light file (no engine requires) so the boot-slimming hit probe can
|
|
19
|
+
# reconstruct `Environment.for_project`'s merged library list — which feeds the run cache KEY's
|
|
20
|
+
# `rbs.libraries` slot — WITHOUT building the RBS environment (or loading the inference engine).
|
|
21
|
+
DEFAULT_LIBRARIES = %w[
|
|
22
|
+
pathname optparse json yaml fileutils tempfile tmpdir
|
|
23
|
+
stringio forwardable digest securerandom
|
|
24
|
+
uri logger date
|
|
25
|
+
pp delegate observable abbrev find tsort singleton
|
|
26
|
+
shellwords benchmark base64 did_you_mean
|
|
27
|
+
monitor mutex_m timeout
|
|
28
|
+
open3 erb etc ipaddr bigdecimal bigdecimal-math
|
|
29
|
+
prettyprint random-formatter time open-uri resolv
|
|
30
|
+
csv pstore objspace io-console cgi cgi-escape
|
|
31
|
+
strscan
|
|
32
|
+
prism rbs
|
|
33
|
+
].freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class Environment
|
|
7
|
+
# ADR-82 WD9 — maps a top-level constant name to the locked, RBS-less gem that declares it, so an
|
|
8
|
+
# unresolved constant read (`Faraday`, `Sidekiq`) can carry the `external_gem_without_rbs` provenance
|
|
9
|
+
# cause instead of the generic `unsupported_syntax`. For a gem with no RBS the constant read is where the
|
|
10
|
+
# class name is last visible — by dispatch time the receiver is already `Dynamic[top]` — so this index is
|
|
11
|
+
# what lets `coverage --protection` route those holes to `add_rbs` honestly.
|
|
12
|
+
#
|
|
13
|
+
# Ownership is established by READING, never by guessing: each gem's conventional entry file
|
|
14
|
+
# (`lib/<name>.rb`, with the dash → directory variant) is parsed with Prism and its top-level
|
|
15
|
+
# class / module / constant declarations recorded under their root name. No gem code runs — the same
|
|
16
|
+
# posture as ADR-72's "loads RBS data only" and the ADR-10 walker. A name-derivation heuristic
|
|
17
|
+
# (`faraday` → `Faraday` by camelizing) is deliberately NOT used: it breaks on irregular names
|
|
18
|
+
# (`activesupport` → `ActiveSupport`) and is exactly the guessing the ADR-82 honesty criterion forbids.
|
|
19
|
+
#
|
|
20
|
+
# Everything fails OPEN to "no owner": a gem that is not installed, an entry file that does not exist or
|
|
21
|
+
# does not parse, a root constant declared only in a deeper file. An unindexed constant keeps today's
|
|
22
|
+
# generic cause — the failure mode is a missing label, never a wrong one.
|
|
23
|
+
#
|
|
24
|
+
# The scan is bounded to entry files (one or two per gem) so building the index over a real app's
|
|
25
|
+
# hundreds of RBS-less gems stays sub-second; it is built lazily on the first unresolved constant, so a
|
|
26
|
+
# project whose constants all resolve never pays it.
|
|
27
|
+
#
|
|
28
|
+
# **Gem-directory resolution.** Rigor runs under its OWN bundle (`BUNDLE_GEMFILE=<rigor>/Gemfile`), so
|
|
29
|
+
# `Gem::Specification.find_by_name` sees rigor's gems, not the target project's — it would resolve only
|
|
30
|
+
# the handful of gems both bundles happen to share (i18n, rack) and miss every project-specific gem
|
|
31
|
+
# (the very ones a Rails app's holes root at). So the primary resolver is the **target's bundle install
|
|
32
|
+
# tree** (`<bundle>/ruby/*/gems/<name>-<version>/`, the same pure-filesystem layout
|
|
33
|
+
# {BundleSigDiscovery} walks — no `Bundler` API, no gem code). `Gem::Specification` remains a last-resort
|
|
34
|
+
# fallback for a project installed against system gems with no discoverable bundle; reading a gem's own
|
|
35
|
+
# entry file for its own top-level namespace constant is version-stable, so even a rigor-vs-target
|
|
36
|
+
# version skew on a shared gem yields the same constant name.
|
|
37
|
+
module MissingGemConstantIndex
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# @param gems [Enumerable<Array(String, String)>] `[gem_name, version]` pairs (the `:missing` rows of
|
|
41
|
+
# {RbsCoverageReport}).
|
|
42
|
+
# @param bundle_path [String, Pathname, nil] the target's resolved bundler install root, or nil.
|
|
43
|
+
# @param spec_resolver [#call] `(name, version) -> String?` fallback dir resolver. Injectable for
|
|
44
|
+
# specs; the default is the RubyGems-metadata lookup (no code load).
|
|
45
|
+
# @return [Hash{String => String}] frozen `root constant name => gem name`. On a collision (two gems
|
|
46
|
+
# declaring the same top-level constant) the first gem wins — the CAUSE recorded downstream
|
|
47
|
+
# (external gem without RBS) is true under either owner.
|
|
48
|
+
def build(gems, bundle_path: nil, spec_resolver: method(:installed_gem_dir))
|
|
49
|
+
bundle_dirs = bundle_gem_dirs(bundle_path)
|
|
50
|
+
index = {}
|
|
51
|
+
gems.each do |gem_name, version|
|
|
52
|
+
dir = bundle_dirs["#{gem_name}-#{version}"] || spec_resolver.call(gem_name, version)
|
|
53
|
+
next unless dir
|
|
54
|
+
|
|
55
|
+
entry_files(dir, gem_name).each do |file|
|
|
56
|
+
top_level_root_constants(file).each { |name| index[name] ||= gem_name }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
index.freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# `{"<name>-<version>" => gem_dir}` for the target's bundle, or `{}` when no bundle is resolvable. The
|
|
63
|
+
# glob mirrors {BundleSigDiscovery}: `<bundle>/ruby/X.Y.Z/gems/<name>-<version>/`. Keyed on the dir
|
|
64
|
+
# basename so a platform-tagged variant (`ffi-1.17.4-aarch64-linux-gnu`) simply doesn't match a
|
|
65
|
+
# `<name>-<version>` lookup — those gems ship native code, not the pure-Ruby constants this indexes.
|
|
66
|
+
def bundle_gem_dirs(bundle_path)
|
|
67
|
+
return {} if bundle_path.nil?
|
|
68
|
+
|
|
69
|
+
base = Pathname.new(bundle_path)
|
|
70
|
+
return {} unless base.directory?
|
|
71
|
+
|
|
72
|
+
Dir.glob(base.join("ruby", "*", "gems", "*")).each_with_object({}) do |dir, acc|
|
|
73
|
+
acc[File.basename(dir)] ||= dir
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# RubyGems spec metadata lookup — the gem's on-disk source root, without loading any of its code.
|
|
78
|
+
# Exact-version first, any-version fallback. See the class note on why this is only a fallback.
|
|
79
|
+
def installed_gem_dir(name, version)
|
|
80
|
+
spec = begin
|
|
81
|
+
Gem::Specification.find_by_name(name, "= #{version}")
|
|
82
|
+
rescue Gem::LoadError
|
|
83
|
+
begin
|
|
84
|
+
Gem::Specification.find_by_name(name)
|
|
85
|
+
rescue Gem::LoadError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
spec&.full_gem_path
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The conventional require targets for a gem name: `lib/foo.rb`, and `lib/foo/bar.rb` for a dashed
|
|
93
|
+
# `foo-bar`. This is the require-name convention Bundler.require depends on — a filename convention,
|
|
94
|
+
# not a constant-name guess; the constants come from parsing whichever of these exists.
|
|
95
|
+
def entry_files(dir, gem_name)
|
|
96
|
+
candidates = ["lib/#{gem_name}.rb"]
|
|
97
|
+
candidates << "lib/#{gem_name.tr('-', '/')}.rb" if gem_name.include?("-")
|
|
98
|
+
candidates.filter_map do |relative|
|
|
99
|
+
path = File.join(dir, relative)
|
|
100
|
+
path if File.file?(path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Root names of the file's top-level declarations. Only direct children of the program are read — the
|
|
105
|
+
# root constant of a gem's namespace is declared at file top level by construction (`module Faraday`,
|
|
106
|
+
# `class Money::Error < …` roots at `Money`), so no recursion into bodies is needed.
|
|
107
|
+
def top_level_root_constants(path)
|
|
108
|
+
result = Prism.parse(File.read(path))
|
|
109
|
+
return [] unless result.errors.empty?
|
|
110
|
+
|
|
111
|
+
result.value.statements.body.filter_map do |node|
|
|
112
|
+
case node
|
|
113
|
+
when Prism::ClassNode, Prism::ModuleNode then root_segment(node.constant_path)
|
|
114
|
+
when Prism::ConstantWriteNode then node.name.to_s
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
rescue StandardError
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def root_segment(node)
|
|
122
|
+
current = node
|
|
123
|
+
current = current.parent while current.is_a?(Prism::ConstantPathNode) && current.parent
|
|
124
|
+
current.respond_to?(:name) && current.name ? current.name.to_s : nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|