rigortype 0.1.19 → 0.2.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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +49 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -132,8 +132,7 @@ module Rigor
|
|
|
132
132
|
# ADR-60 WD3 record-and-validate: the producer's in-block
|
|
133
133
|
# `AttachmentDiscoverer` reads are captured into the
|
|
134
134
|
# dependency descriptor after the block runs, and the
|
|
135
|
-
# producer's `watch:` covers model-file additions
|
|
136
|
-
# priming walk is needed (it used to run the discover twice).
|
|
135
|
+
# producer's `watch:` covers model-file additions.
|
|
137
136
|
@attachment_index = cache_for(:attachment_index, params: {}).call
|
|
138
137
|
rescue Plugin::AccessDeniedError => e
|
|
139
138
|
@load_errors << "rigor-activestorage: #{e.message}"
|
|
@@ -44,19 +44,17 @@ module Rigor
|
|
|
44
44
|
# `send_reset_password_instructions`, etc. resolve through the
|
|
45
45
|
# synthetic-method tier without `call.undefined-method`.
|
|
46
46
|
#
|
|
47
|
-
# ##
|
|
47
|
+
# ## Precision tier
|
|
48
48
|
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
# provenance field is recorded so the ceiling slice can
|
|
57
|
-
# promote without rescanning.
|
|
49
|
+
# The scanner records `origin_module:` in each synthetic
|
|
50
|
+
# method's provenance. The dispatcher's slice-6a TierB path
|
|
51
|
+
# (`promote_via_origin_module`) redispatches on
|
|
52
|
+
# `Nominal[origin_module]` via `RbsDispatch`, so Devise's
|
|
53
|
+
# authored RBS return types win: `valid_password?` returns
|
|
54
|
+
# `bool`, not `Dynamic[T]`. Unknown return types degrade
|
|
55
|
+
# gracefully to `Dynamic[T]`.
|
|
58
56
|
#
|
|
59
|
-
# ## Scope
|
|
57
|
+
# ## Scope
|
|
60
58
|
#
|
|
61
59
|
# - Recognises model-side `devise :a, :b` on any AR::Base
|
|
62
60
|
# subclass; trait symbol set mirrors `lib/devise/modules.rb`.
|
|
@@ -35,16 +35,15 @@ module Rigor
|
|
|
35
35
|
# other files then dispatch through the synthetic record rather
|
|
36
36
|
# than falling through to `call.undefined-method`.
|
|
37
37
|
#
|
|
38
|
-
# ##
|
|
38
|
+
# ## Precision model (ADR-16 WD13 + ADR-18)
|
|
39
39
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# return shape; slice 6 unlocks precision without re-authoring.
|
|
40
|
+
# The synthetic reader's return type is resolved via ADR-18's
|
|
41
|
+
# `returns_from_arg:` fact lookup: the call-site's second argument
|
|
42
|
+
# (`Types::String` etc.) is looked up through the `:dry_type_aliases`
|
|
43
|
+
# fact published by `rigor-dry-types`, yielding `Nominal[String]`
|
|
44
|
+
# for common cases. When the lookup misses (e.g. inline method-chain
|
|
45
|
+
# argument whose chain-head isn't currently extracted), the row
|
|
46
|
+
# falls back to `Dynamic[Top]` silently.
|
|
48
47
|
#
|
|
49
48
|
# ## Scope (slice 2c minimum)
|
|
50
49
|
#
|
|
@@ -22,9 +22,8 @@ module Rigor
|
|
|
22
22
|
# Other dry-rb adapter plugins consume this fact:
|
|
23
23
|
#
|
|
24
24
|
# - `rigor-dry-struct` reads it so `attribute :city, Types::String`
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# resolver chain).
|
|
25
|
+
# promotes `address.city` from `Dynamic[Top]` to `Nominal[String]`
|
|
26
|
+
# via ADR-18's `returns_from_arg:` fact lookup.
|
|
28
27
|
# - `rigor-dry-validation` / `rigor-dry-schema` read it for
|
|
29
28
|
# per-key type recognition in `schema { … }` / `params { … }`
|
|
30
29
|
# blocks (separate plugin slice).
|
|
@@ -43,17 +42,19 @@ module Rigor
|
|
|
43
42
|
# "<UnderlyingClass>" }` so consumers can match on the
|
|
44
43
|
# qualified constant name they see in source.
|
|
45
44
|
#
|
|
46
|
-
#
|
|
45
|
+
# Implemented beyond the floor:
|
|
47
46
|
#
|
|
48
|
-
# -
|
|
47
|
+
# - Nested-namespace aliases (`Types::Coercible::Integer`,
|
|
49
48
|
# `Types::Strict::Symbol`, `Types::Params::Bool`,
|
|
50
|
-
# `Types::JSON::Date`) —
|
|
51
|
-
#
|
|
52
|
-
# -
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
49
|
+
# `Types::JSON::Date`) — the four coercion categories each
|
|
50
|
+
# map to the same underlying class as their canonical shortcut.
|
|
51
|
+
# - User-authored compositions (`Email = Types::String.constrained(...)`,
|
|
52
|
+
# transitive resolution through composition chains) — the alias
|
|
53
|
+
# surface extends beyond the 15 canonical names.
|
|
54
|
+
#
|
|
55
|
+
# Deferred:
|
|
56
|
+
#
|
|
57
|
+
# - `dry-types.unknown-alias` / `dry-types.alias-shadow`
|
|
57
58
|
# diagnostics when downstream code references a name that
|
|
58
59
|
# wasn't published.
|
|
59
60
|
#
|
|
@@ -21,10 +21,9 @@ module Rigor
|
|
|
21
21
|
# cross-plugin fact.
|
|
22
22
|
# - Ships an RBS overlay (`sig/dry_validation.rbs`) typing
|
|
23
23
|
# `Dry::Validation::Contract#call` (returns Result) and
|
|
24
|
-
# `Dry::Validation::Result#{success?, failure?, to_h}`.
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# for the wiring step.
|
|
24
|
+
# `Dry::Validation::Result#{success?, failure?, to_h}`.
|
|
25
|
+
# The manifest's `signature_paths: ["sig"]` auto-contributes
|
|
26
|
+
# the overlay (ADR-25) — no project-side wiring needed.
|
|
28
27
|
#
|
|
29
28
|
# Slice 2 (deferred, per design note):
|
|
30
29
|
#
|
|
@@ -86,14 +86,14 @@ module Rigor
|
|
|
86
86
|
# a `Prism::SymbolNode` is treated as a literal
|
|
87
87
|
# attribute reference.
|
|
88
88
|
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
89
|
+
# When `model_index` (the cross-plugin `:model_index`
|
|
90
|
+
# fact published by rigor-activerecord) is present,
|
|
91
|
+
# the effective accepted key set is the UNION of the
|
|
92
|
+
# factory's declared attributes plus the corresponding
|
|
93
|
+
# model's columns. FactoryBot's runtime accepts any AR
|
|
94
|
+
# attribute regardless of whether the factory declared
|
|
95
|
+
# it, so the cross-check broadens the acceptance
|
|
96
|
+
# accordingly.
|
|
97
97
|
def unknown_attribute_violations(call_node, entry, model_index)
|
|
98
98
|
accepted_keys, suggestion_dictionary = effective_keys(entry, model_index)
|
|
99
99
|
attr_spell_checker = DidYouMean::SpellChecker.new(dictionary: suggestion_dictionary)
|
|
@@ -23,8 +23,8 @@ module Rigor
|
|
|
23
23
|
# - `factory :users, aliases: [:author] do ... end` — alias form
|
|
24
24
|
#
|
|
25
25
|
# Inside a factory block, attribute declarations come in
|
|
26
|
-
# several shapes.
|
|
27
|
-
#
|
|
26
|
+
# several shapes. Only literal-name forms are recognised
|
|
27
|
+
# (Symbol arg / String arg):
|
|
28
28
|
#
|
|
29
29
|
# - `name { "Alice" }` — implicit attribute via
|
|
30
30
|
# `method_missing` with a block (FactoryBot's modern
|
|
@@ -116,8 +116,8 @@ module Rigor
|
|
|
116
116
|
Rigor::Source::Literals.symbol_or_string_name(call_node.arguments&.arguments&.first)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
119
|
+
# Resolves the model class name for the factory.
|
|
120
|
+
# Three sources, in priority order:
|
|
121
121
|
#
|
|
122
122
|
# 1. Explicit `class: <Const>` keyword arg —
|
|
123
123
|
# ConstantReadNode / ConstantPathNode value.
|
|
@@ -188,11 +188,10 @@ module Rigor
|
|
|
188
188
|
attributes
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
# Walks the block body collecting attribute names.
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# (traits ship in a follow-up).
|
|
191
|
+
# Walks the block body collecting attribute names. Only
|
|
192
|
+
# top-level statements are examined — attributes inside
|
|
193
|
+
# `trait :admin do ... end` or other nested blocks are
|
|
194
|
+
# not collected (traits deferred to a follow-up).
|
|
196
195
|
def collect_attributes_from(node, accumulator)
|
|
197
196
|
return unless node.is_a?(Prism::Node)
|
|
198
197
|
|
|
@@ -206,8 +205,7 @@ module Rigor
|
|
|
206
205
|
def record_attribute(node, accumulator)
|
|
207
206
|
return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
208
207
|
# Skip association / sequence / trait / framework
|
|
209
|
-
# methods —
|
|
210
|
-
# declarations.
|
|
208
|
+
# methods — only plain attribute declarations are recorded.
|
|
211
209
|
return if SKIPPED_METHODS.include?(node.name)
|
|
212
210
|
|
|
213
211
|
name = if node.name == :add_attribute
|
|
@@ -4,15 +4,14 @@ module Rigor
|
|
|
4
4
|
module Plugin
|
|
5
5
|
class Factorybot < Rigor::Plugin::Base
|
|
6
6
|
# Per-run frozen index of discovered FactoryBot factories
|
|
7
|
-
# and the attribute keys each declares.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# and the attribute keys each declares. Indexes only
|
|
8
|
+
# **literal symbol/string** factory names + **literal
|
|
9
|
+
# symbol** attribute names; sequences, parent/child
|
|
10
|
+
# relationships, traits, and dynamically-named factories
|
|
11
|
+
# are deferred to follow-up slices.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# builds. Resolved from:
|
|
13
|
+
# Each entry carries a `model_class` — the inferred or
|
|
14
|
+
# explicit class the factory builds. Resolved from:
|
|
16
15
|
#
|
|
17
16
|
# 1. An explicit `factory :user, class: User do`
|
|
18
17
|
# keyword option (ConstantReadNode / ConstantPathNode
|
|
@@ -13,12 +13,10 @@ module Rigor
|
|
|
13
13
|
# attributes_for / *_list family against a per-run index
|
|
14
14
|
# built from `factory_search_paths`.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# `rigor-activerecord` `:model_index` ADR-9 fact, after
|
|
21
|
-
# `rigor-activerecord` adds the matching publish hook.
|
|
16
|
+
# Recognises factory NAMES + literal ATTRIBUTE KEYS in
|
|
17
|
+
# the call's keyword hash. The AR column cross-check uses
|
|
18
|
+
# the `rigor-activerecord` `:model_index` ADR-9 fact when
|
|
19
|
+
# that plugin is loaded (optional `consumes:`).
|
|
22
20
|
# Traits, sequences, parent / child factories, and dynamic
|
|
23
21
|
# factory names are deferred to follow-up slices.
|
|
24
22
|
#
|
|
@@ -54,9 +52,9 @@ module Rigor
|
|
|
54
52
|
# `.build_stubbed_list`. The legacy `FactoryGirl` constant
|
|
55
53
|
# is recognised identically. Implicit-receiver calls
|
|
56
54
|
# (`create(:name)` inside an `include FactoryBot::Syntax::Methods`
|
|
57
|
-
# context) are NOT recognised
|
|
58
|
-
#
|
|
59
|
-
#
|
|
55
|
+
# context) are NOT recognised — too many false positives on
|
|
56
|
+
# plain `create` calls outside test files; deferred until
|
|
57
|
+
# receiver-type inference can disambiguate.
|
|
60
58
|
#
|
|
61
59
|
# ## What's recognised inside `factory :name do ... end`
|
|
62
60
|
#
|
|
@@ -50,12 +50,13 @@ module Rigor
|
|
|
50
50
|
|
|
51
51
|
# @param paths [Array<String>] absolute paths to `.rb` files
|
|
52
52
|
# the project's `paths:` resolves to.
|
|
53
|
-
# @return [Hash{Symbol => Hash}] frozen
|
|
53
|
+
# @return [Hash{Symbol => Hash}] frozen 4-key result:
|
|
54
54
|
# `:types` (per-`Schema::Object` field table),
|
|
55
|
-
# `:enums` (per-`Schema::Enum` value list),
|
|
56
|
-
# `:input_objects` (per-`Schema::InputObject` argument
|
|
57
|
-
# table).
|
|
58
|
-
#
|
|
55
|
+
# `:enums` (per-`Schema::Enum` value list),
|
|
56
|
+
# `:input_objects` (per-`Schema::InputObject` argument table),
|
|
57
|
+
# `:mutations` (per-`Schema::Mutation` arguments+fields table).
|
|
58
|
+
# Any subset may be empty when no recognisable declaration
|
|
59
|
+
# of that kind is found.
|
|
59
60
|
def scan(paths:)
|
|
60
61
|
acc = empty_accumulator
|
|
61
62
|
paths.each do |path|
|
|
@@ -97,10 +98,9 @@ module Rigor
|
|
|
97
98
|
private_class_method :scan_file
|
|
98
99
|
|
|
99
100
|
# Walks the AST collecting `class X < GraphQL::Schema::Object`,
|
|
100
|
-
# `
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# publish multiple cross-plugin facts from one walk.
|
|
101
|
+
# `Schema::Enum`, `Schema::InputObject`, and `Schema::Mutation`
|
|
102
|
+
# decls at any nesting level. Returns a 4-key hash so the
|
|
103
|
+
# caller can publish multiple cross-plugin facts from one walk.
|
|
104
104
|
def collect_definitions(node, qualified_prefix)
|
|
105
105
|
return empty_accumulator if node.nil?
|
|
106
106
|
|
|
@@ -203,10 +203,9 @@ module Rigor
|
|
|
203
203
|
# The first positional must be a String literal — the
|
|
204
204
|
# graphql-ruby `value` API also accepts a Symbol form
|
|
205
205
|
# (`value :ACTIVE`) but the documented idiom is String.
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
# the floor.
|
|
206
|
+
# Only the GraphQL-side value name is stored; the optional
|
|
207
|
+
# `value:` kwarg (Ruby-side override) and `description:`
|
|
208
|
+
# are omitted from the published table.
|
|
210
209
|
def collect_values(body)
|
|
211
210
|
return [] if body.nil?
|
|
212
211
|
|
|
@@ -25,10 +25,10 @@ module Rigor
|
|
|
25
25
|
# resolver methods themselves; rigor's value here is producing a
|
|
26
26
|
# static type table downstream consumers can cross-reference.
|
|
27
27
|
#
|
|
28
|
-
# ## What downstream consumers DO with
|
|
28
|
+
# ## What downstream consumers DO with the published facts
|
|
29
29
|
#
|
|
30
|
-
# The
|
|
31
|
-
# demand-driven,
|
|
30
|
+
# The tables are the substrate for two future capabilities
|
|
31
|
+
# (demand-driven, not yet implemented):
|
|
32
32
|
#
|
|
33
33
|
# - Resolver-method check: for each `field :name, Type` whose
|
|
34
34
|
# `name` is also defined as a Ruby method on the class, verify
|
|
@@ -37,33 +37,25 @@ module Rigor
|
|
|
37
37
|
# plugin could type `Schema.execute(query).to_h` against the
|
|
38
38
|
# queried fields.
|
|
39
39
|
#
|
|
40
|
-
# ##
|
|
40
|
+
# ## What's recognised
|
|
41
41
|
#
|
|
42
|
-
#
|
|
42
|
+
# - `class T < GraphQL::Schema::Object` subclasses (including
|
|
43
|
+
# nested namespaces); `field :name, Type, null: ...`
|
|
44
|
+
# declarations with constant-reference or list-array types
|
|
45
|
+
# and GraphQL→Ruby scalar mapping.
|
|
46
|
+
# - `class T < GraphQL::Schema::Enum`; `value "ACTIVE"` calls.
|
|
47
|
+
# - `class T < GraphQL::Schema::InputObject` /
|
|
48
|
+
# `GraphQL::Schema::Mutation`; `argument :name, Type,
|
|
49
|
+
# required: ...` declarations.
|
|
50
|
+
# - No user-facing diagnostics yet.
|
|
43
51
|
#
|
|
44
|
-
#
|
|
45
|
-
# (including nested namespaces: `class Types::User < ...`,
|
|
46
|
-
# `module Types; class User < ...; end; end`).
|
|
47
|
-
# - Recognises the `field :name, Type, **opts` declaration with:
|
|
48
|
-
# - `Type` as a `ConstantReadNode` / `ConstantPathNode` (`String`
|
|
49
|
-
# / `Integer` / `Boolean` / `Float` / `ID`, or a user-defined
|
|
50
|
-
# `Types::OtherObject`).
|
|
51
|
-
# - `null: true` / `null: false` keyword extracts nullability.
|
|
52
|
-
# - Maps the canonical GraphQL scalar names to underlying Ruby
|
|
53
|
-
# classes (`String` → `String`, `Integer` → `Integer`,
|
|
54
|
-
# `Boolean` → `TrueClass`, `Float` → `Float`, `ID` → `String`).
|
|
55
|
-
# - Publishes the table; no user-facing diagnostics yet.
|
|
52
|
+
# ## Deferred (demand-driven)
|
|
56
53
|
#
|
|
57
|
-
# The **ceiling** (future slices, demand-driven):
|
|
58
|
-
#
|
|
59
|
-
# - **`GraphQL::Schema::Enum`** with `value "ACTIVE"` calls.
|
|
60
|
-
# - **`GraphQL::Schema::Mutation`** + **`GraphQL::Schema::InputObject`**.
|
|
61
|
-
# - **List / Non-Null wrappers** (`[String]`, `String.array`).
|
|
62
54
|
# - **`resolver:` / `mutation:` reroute** recognition.
|
|
63
55
|
# - **String type expressions** (`field :foo, "User"`) — defeats
|
|
64
56
|
# static resolution by design (graphql-ruby's `BuildType.parse_type`
|
|
65
57
|
# constantizes at runtime); a future slice could surface these
|
|
66
|
-
# as `graphql.string-type` `:info` diagnostics
|
|
58
|
+
# as `graphql.string-type` `:info` diagnostics pointing the
|
|
67
59
|
# user at the constant-reference form for static typing.
|
|
68
60
|
class Graphql < Rigor::Plugin::Base
|
|
69
61
|
manifest(
|
|
@@ -71,9 +71,9 @@ module Rigor
|
|
|
71
71
|
# - `is_a?(Result::Ok)` / `Some` / `None` exhaustive
|
|
72
72
|
# narrowing — core control-flow analysis over a sealed
|
|
73
73
|
# hierarchy, not a plugin surface.
|
|
74
|
-
# - The `variants do variant Const, Type end` Enum DSL
|
|
75
|
-
#
|
|
76
|
-
#
|
|
74
|
+
# - The `variants do variant Const, Type end` Enum DSL is handled
|
|
75
|
+
# via ADR-36 `nested_class_templates:` in this plugin's manifest
|
|
76
|
+
# (Slice A — see `nested_class_templates:` block below).
|
|
77
77
|
class Mangrove < Rigor::Plugin::Base
|
|
78
78
|
manifest(
|
|
79
79
|
id: "mangrove",
|
|
@@ -30,10 +30,10 @@ module Rigor
|
|
|
30
30
|
#
|
|
31
31
|
# ## Configuration
|
|
32
32
|
#
|
|
33
|
-
# No knobs
|
|
34
|
-
# in `.rigor.yml`.
|
|
33
|
+
# No configuration knobs. Activate via
|
|
34
|
+
# `plugins: ["rigor-minitest"]` in `.rigor.yml`.
|
|
35
35
|
#
|
|
36
|
-
# ## Limitations
|
|
36
|
+
# ## Limitations
|
|
37
37
|
#
|
|
38
38
|
# - **No `assert_raises(T) { ... }`** — that's a block-shape
|
|
39
39
|
# matcher and Rigor's narrowing model is for
|
|
@@ -169,7 +169,7 @@ module Rigor
|
|
|
169
169
|
|
|
170
170
|
# Extracts the literal-string first argument when
|
|
171
171
|
# present. Returns nil for variable / expression keys —
|
|
172
|
-
#
|
|
172
|
+
# only literal keys are statically validated.
|
|
173
173
|
def literal_key_for(call_node)
|
|
174
174
|
args = call_node.arguments&.arguments || []
|
|
175
175
|
return nil if args.empty?
|
|
@@ -224,9 +224,7 @@ module Rigor
|
|
|
224
224
|
|
|
225
225
|
def collect_assoc_keys(hash_node)
|
|
226
226
|
# Both `Prism::HashNode` and `Prism::KeywordHashNode`
|
|
227
|
-
# expose `#elements
|
|
228
|
-
# accidental no-op carried over from an earlier
|
|
229
|
-
# draft.
|
|
227
|
+
# expose `#elements`, so a single path handles both.
|
|
230
228
|
hash_node.elements.filter_map do |element|
|
|
231
229
|
next nil unless element.is_a?(Prism::AssocNode)
|
|
232
230
|
|
|
@@ -57,7 +57,7 @@ module Rigor
|
|
|
57
57
|
parsed.each do |locale, tree|
|
|
58
58
|
locale = locale.to_s
|
|
59
59
|
locales << locale
|
|
60
|
-
|
|
60
|
+
each_flattened(tree, []) do |dotted_key, value|
|
|
61
61
|
placeholders = (per_key[dotted_key] ||= {})
|
|
62
62
|
placeholders[locale] = extract_placeholders(value)
|
|
63
63
|
kinds = (per_key_kinds[dotted_key] ||= {})
|
|
@@ -101,24 +101,40 @@ module Rigor
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Recursively walks the per-locale subtree, yielding
|
|
104
|
-
# `[dotted_key, leaf_value]`
|
|
104
|
+
# `[dotted_key, leaf_value]` for each leaf. Hash leaves are
|
|
105
105
|
# *not* recorded as entries themselves — only their
|
|
106
|
-
# descendants — but every leaf scalar / array IS
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
# descendants — but every leaf scalar / array IS recorded.
|
|
107
|
+
#
|
|
108
|
+
# `breadcrumbs` is a single mutable stack reused across the
|
|
109
|
+
# whole walk (push before recursing, pop after): for the
|
|
110
|
+
# 530-file / 14 MB Mastodon locale corpus the old
|
|
111
|
+
# `flat_map { flatten_tree(v, breadcrumbs + [k]) }` shape
|
|
112
|
+
# allocated a fresh breadcrumb Array at every node plus an
|
|
113
|
+
# intermediate result Array at every level — millions of
|
|
114
|
+
# short-lived objects and the run's top allocation site. The
|
|
115
|
+
# dotted key is still materialised once per leaf (it has to
|
|
116
|
+
# be); everything else is now allocation-free traversal.
|
|
117
|
+
def each_flattened(node, breadcrumbs, &)
|
|
118
|
+
if node.is_a?(Hash)
|
|
119
|
+
node.each do |k, v|
|
|
120
|
+
breadcrumbs.push(k.to_s)
|
|
121
|
+
each_flattened(v, breadcrumbs, &)
|
|
122
|
+
breadcrumbs.pop
|
|
113
123
|
end
|
|
114
124
|
else
|
|
115
|
-
|
|
125
|
+
yield breadcrumbs.join("."), node
|
|
116
126
|
end
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
def extract_placeholders(value)
|
|
120
130
|
case value
|
|
121
|
-
when String
|
|
131
|
+
when String
|
|
132
|
+
# Most locale leaves carry no `%{var}`; skip the scan +
|
|
133
|
+
# flatten + to_set allocation trio for them. A string with
|
|
134
|
+
# no `%{` yields an empty placeholder set either way.
|
|
135
|
+
return Set.new unless value.include?("%{")
|
|
136
|
+
|
|
137
|
+
value.scan(PLACEHOLDER_RE).flatten.to_set
|
|
122
138
|
when Array then value.map { |v| extract_placeholders(v) }.reduce(Set.new) { |a, s| a | s }
|
|
123
139
|
else Set.new
|
|
124
140
|
end
|
|
@@ -40,7 +40,7 @@ module Rigor
|
|
|
40
40
|
# arguments. Missing placeholders are errors; extra
|
|
41
41
|
# arguments are warnings.
|
|
42
42
|
#
|
|
43
|
-
# ## Limitations
|
|
43
|
+
# ## Limitations
|
|
44
44
|
#
|
|
45
45
|
# - Only literal-string keys are validated. `t(key)` with
|
|
46
46
|
# a variable receiver is silently passed through.
|
|
@@ -38,11 +38,9 @@ module Rigor
|
|
|
38
38
|
# (`user_facebook_omniauth_authorize_path`) and Devise
|
|
39
39
|
# declares them from the configured providers, which
|
|
40
40
|
# live in an initializer this static parser does not
|
|
41
|
-
# read.
|
|
42
|
-
# `
|
|
43
|
-
#
|
|
44
|
-
# these are NOT in the table but consulted by the
|
|
45
|
-
# `Analyzer.allowed_dynamic_pattern?` check.
|
|
41
|
+
# read. The suffix patterns are registered in
|
|
42
|
+
# `OMNIAUTH_SUFFIXES` and consulted by
|
|
43
|
+
# `HelperTable#omniauth_match?`.
|
|
46
44
|
module DeviseRoutes
|
|
47
45
|
# The standard Devise controllers and the helper
|
|
48
46
|
# actions each generates. Keys are the controller
|
|
@@ -190,7 +188,7 @@ module Rigor
|
|
|
190
188
|
|
|
191
189
|
# Returns the set of OmniAuth pattern suffixes the
|
|
192
190
|
# analyzer accepts for a given resource. The analyzer
|
|
193
|
-
# consults this set (via `HelperTable#
|
|
191
|
+
# consults this set (via `HelperTable#omniauth_match?`)
|
|
194
192
|
# when a `*_path` / `*_url` call's name does not match
|
|
195
193
|
# any registered entry and its prefix matches a Devise
|
|
196
194
|
# resource.
|
|
@@ -1171,9 +1171,10 @@ module Rigor
|
|
|
1171
1171
|
end
|
|
1172
1172
|
|
|
1173
1173
|
def in_singular_resource?(*)
|
|
1174
|
-
#
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1174
|
+
# Stub: always returns true so member / collection
|
|
1175
|
+
# blocks descend. The singular-resource frame
|
|
1176
|
+
# (`push_singular_resource`) is modelled in Context;
|
|
1177
|
+
# a future caller could use it to tighten this check.
|
|
1177
1178
|
true
|
|
1178
1179
|
end
|
|
1179
1180
|
|
|
@@ -1181,21 +1182,14 @@ module Rigor
|
|
|
1181
1182
|
# `plural: true` for `resources :users`, `false` for
|
|
1182
1183
|
# `resource :profile`.
|
|
1183
1184
|
def register_resourceful_helpers(name, actions, base_arity, context, plural:)
|
|
1184
|
-
#
|
|
1185
|
-
#
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
# `relationship_path
|
|
1191
|
-
#
|
|
1192
|
-
# Plural resources singularise for show / new / edit
|
|
1193
|
-
# helpers (`resources :users` → `user_path(id)`);
|
|
1194
|
-
# singular resources use the name AS-IS even when it
|
|
1195
|
-
# looks plural (Mastodon's `resource :relationships,
|
|
1196
|
-
# only: [:show, :update]` → `relationships_path`).
|
|
1197
|
-
# The path segment uses `name` in both shapes — Rails
|
|
1198
|
-
# never singularises the URL.
|
|
1185
|
+
# Plural resources (`resources :users`) singularise
|
|
1186
|
+
# for show / new / edit helpers → `user_path(id)`.
|
|
1187
|
+
# Singular resources (`resource :foo`) use the name
|
|
1188
|
+
# AS-IS — singularising would mangle deliberately-
|
|
1189
|
+
# plural names like Mastodon's `resource
|
|
1190
|
+
# :relationships` → `relationships_path`, not
|
|
1191
|
+
# `relationship_path`. The URL path uses `name`
|
|
1192
|
+
# in both shapes (Rails never singularises the URL).
|
|
1199
1193
|
singular = plural ? singularize_word(name.to_s) : name.to_s
|
|
1200
1194
|
path_base = "#{context.path_prefix}/#{name}"
|
|
1201
1195
|
|
|
@@ -26,9 +26,9 @@ module Rigor
|
|
|
26
26
|
# - One level of nested `resources`
|
|
27
27
|
#
|
|
28
28
|
# The plugin publishes its parsed `:helper_table` through
|
|
29
|
-
# the ADR-9 cross-plugin fact store
|
|
30
|
-
#
|
|
31
|
-
#
|
|
29
|
+
# the ADR-9 cross-plugin fact store; `rigor-actionpack`
|
|
30
|
+
# Phase 4 consumes it for route-helper validation in
|
|
31
|
+
# controller code.
|
|
32
32
|
#
|
|
33
33
|
# ## Configuration
|
|
34
34
|
#
|
|
@@ -147,8 +147,8 @@ module Rigor
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
# Publishes the parsed table to the cross-plugin fact
|
|
150
|
-
# store
|
|
151
|
-
#
|
|
150
|
+
# store; `rigor-actionpack` Phase 4 reads it via
|
|
151
|
+
# `services.fact_store.read`.
|
|
152
152
|
def prepare(services)
|
|
153
153
|
table = helper_table_or_nil
|
|
154
154
|
return if table.nil?
|
|
@@ -21,10 +21,9 @@ module Rigor
|
|
|
21
21
|
# { ... }` and `subject(:name) { ... }` declarations.
|
|
22
22
|
# `:subject` is the key for the implicit `subject { ... }`.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# block's inferred type.
|
|
24
|
+
# Used by the plugin's let-binding `dynamic_return`
|
|
25
|
+
# rule to bind `let`-named method-shape calls inside
|
|
26
|
+
# `it` bodies to the let block's inferred type.
|
|
28
27
|
class LetScopeIndex
|
|
29
28
|
Record = Struct.new(:outer_range, :describe_const, :lets, keyword_init: true) do
|
|
30
29
|
def contains?(line) = outer_range.cover?(line)
|
|
@@ -6,7 +6,7 @@ require "rigor/type"
|
|
|
6
6
|
module Rigor
|
|
7
7
|
module Plugin
|
|
8
8
|
class Rspec < Rigor::Plugin::Base
|
|
9
|
-
#
|
|
9
|
+
# Resolves the runtime type of a
|
|
10
10
|
# `let(:name) { body }` or `subject(:name) { body }`
|
|
11
11
|
# block by pattern-matching its body's tail expression.
|
|
12
12
|
#
|
|
@@ -8,7 +8,7 @@ require "rigor/flow_contribution/fact"
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Plugin
|
|
10
10
|
class Rspec < Rigor::Plugin::Base
|
|
11
|
-
#
|
|
11
|
+
# Recognises `expect(x).to MATCHER`
|
|
12
12
|
# patterns at per-call recognition time and emits
|
|
13
13
|
# `post_return_facts` that narrow the named local on the
|
|
14
14
|
# post-call edge.
|