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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Source
|
|
7
|
+
# Allocation-free replacement for the `node.compact_child_nodes.each { … }` idiom.
|
|
8
|
+
#
|
|
9
|
+
# `Prism::Node#compact_child_nodes` allocates a fresh Array on every call — for the ~43 leaf node classes
|
|
10
|
+
# (`IntegerNode`, `LocalVariableReadNode`, `NilNode`, …) it is literally `def compact_child_nodes; []; end`.
|
|
11
|
+
# Rigor's tree walkers call it unconditionally on every node of every walk, so one full-tree walk allocates one
|
|
12
|
+
# Array per node visited. On leaf-heavy sources (a Ragel-generated parser has hundreds of thousands of
|
|
13
|
+
# integer-literal leaves) these throwaway arrays are the single largest allocation source in a run — over half
|
|
14
|
+
# of all allocations on mail's `lib`.
|
|
15
|
+
#
|
|
16
|
+
# Loading this file compiles a `#rigor_each_child` method onto every `Prism::*Node` class (additive reopening of
|
|
17
|
+
# a dependency's class, the `Cache::RbsEnvironmentMarshalPatch` precedent; the `rigor_` prefix keeps it
|
|
18
|
+
# collision-free). It yields the same child nodes, in the same order, without materialising the intermediate
|
|
19
|
+
# array: each child-bearing field is read directly in field declaration order — exactly the order
|
|
20
|
+
# `compact_child_nodes` emits — and a `NodeListField`'s already-materialised array is iterated in place (the
|
|
21
|
+
# reader returns the stored array, not a copy). Nil optional children and nil list elements are skipped,
|
|
22
|
+
# mirroring `compact_child_nodes`'s "compact" semantics. Leaf classes compile to an empty method.
|
|
23
|
+
#
|
|
24
|
+
# A method compiled onto the node class is the fastest dispatch available for this shape — one virtual send on
|
|
25
|
+
# the receiver, the same mechanism `compact_child_nodes` itself uses, with zero allocation. The reflective
|
|
26
|
+
# alternatives were measured and rejected: a `public_send`-per-field loop is ~45 % *slower* than
|
|
27
|
+
# `compact_child_nodes` on a full-tree walk (dynamic dispatch per field swamps the allocation win), and a
|
|
28
|
+
# central generated-method table dispatched via `Module#send` + a per-class Hash lookup still regressed
|
|
29
|
+
# Rails-corpus wall ~5 % under YJIT. Walkers therefore call `node.rigor_each_child { … }` directly.
|
|
30
|
+
#
|
|
31
|
+
# The field map is derived once at load from `Prism::Reflection`, so it tracks whatever `prism` version resolves
|
|
32
|
+
# at runtime (ADR-79) instead of a hand-maintained table. `spec/rigor/source/node_children_spec.rb` is the
|
|
33
|
+
# binding contract: over a corpus of real source it asserts — for every node reached — that both
|
|
34
|
+
# `#rigor_each_child` and {each_child} yield output element-for-element identical (object identity and order) to
|
|
35
|
+
# `compact_child_nodes`.
|
|
36
|
+
module NodeChildren
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# The concrete `Prism::*Node` classes (`< Prism::Node`).
|
|
40
|
+
NODE_CLASSES =
|
|
41
|
+
Prism.constants.grep(/Node\z/).filter_map do |name|
|
|
42
|
+
const = Prism.const_get(name)
|
|
43
|
+
const if const.is_a?(Class) && const < Prism::Node
|
|
44
|
+
end.freeze
|
|
45
|
+
|
|
46
|
+
# Shared frozen empty entry for leaf classes. Identity (`equal?`) distinguishes a leaf entry from a
|
|
47
|
+
# childless-in-practice non-leaf.
|
|
48
|
+
EMPTY = [].freeze
|
|
49
|
+
|
|
50
|
+
# node class => frozen Array of `[reader_symbol, :node | :node_optional | :list]` pairs, in field declaration
|
|
51
|
+
# order (the order `compact_child_nodes` emits its children). Leaf classes map to {EMPTY}. This is the source
|
|
52
|
+
# of truth the per-class methods are compiled from, exposed for introspection and the equivalence spec.
|
|
53
|
+
CHILD_READERS =
|
|
54
|
+
NODE_CLASSES.each_with_object({}) do |klass, map|
|
|
55
|
+
pairs = Prism::Reflection.fields_for(klass).filter_map do |field|
|
|
56
|
+
case field
|
|
57
|
+
when Prism::Reflection::OptionalNodeField
|
|
58
|
+
[field.name, :node_optional].freeze
|
|
59
|
+
when Prism::Reflection::NodeField
|
|
60
|
+
[field.name, :node].freeze
|
|
61
|
+
when Prism::Reflection::NodeListField
|
|
62
|
+
[field.name, :list].freeze
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
map[klass] = pairs.empty? ? EMPTY : pairs.freeze
|
|
66
|
+
end.freeze
|
|
67
|
+
|
|
68
|
+
# The leaf classes — no child-bearing field, so `compact_child_nodes` is always `[]` and `#rigor_each_child`
|
|
69
|
+
# compiles to an empty method. Exposed for the equivalence spec and introspection.
|
|
70
|
+
LEAF_CLASSES = NODE_CLASSES.select { |klass| CHILD_READERS[klass].equal?(EMPTY) }.to_set.freeze
|
|
71
|
+
|
|
72
|
+
# Codegen (not per-call reflection) so iteration reads fields directly at hand-written speed — see the module
|
|
73
|
+
# comment for the measurements ruling out the reflective forms. Each statement mirrors `compact_child_nodes`'s
|
|
74
|
+
# own form for that field kind exactly: a required `NodeField` is yielded unconditionally (`compact <<
|
|
75
|
+
# predicate`), an `OptionalNodeField` behind a truthiness guard (`compact << receiver if receiver`), and a
|
|
76
|
+
# `NodeListField`'s stored array is iterated unfiltered (`compact.concat(requireds)`) — no extra nil checks
|
|
77
|
+
# beyond what Prism itself performs. The explicit `self.` receiver keeps a hypothetical future field named
|
|
78
|
+
# `child` from parsing as the just-assigned local. For a `CallNode` (three optional-node fields) the emitted
|
|
79
|
+
# source is:
|
|
80
|
+
#
|
|
81
|
+
# def rigor_each_child
|
|
82
|
+
# child = self.receiver
|
|
83
|
+
# yield child if child
|
|
84
|
+
# child = self.arguments
|
|
85
|
+
# yield child if child
|
|
86
|
+
# child = self.block
|
|
87
|
+
# yield child if child
|
|
88
|
+
# end
|
|
89
|
+
NODE_CLASSES.each do |klass|
|
|
90
|
+
statements = CHILD_READERS[klass].map do |reader, kind|
|
|
91
|
+
case kind
|
|
92
|
+
when :list then "self.#{reader}.each { |child| yield child }"
|
|
93
|
+
when :node_optional then "child = self.#{reader}\n yield child if child"
|
|
94
|
+
else "yield self.#{reader}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
klass.class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
98
|
+
def rigor_each_child
|
|
99
|
+
#{statements.join("\n ")}
|
|
100
|
+
end
|
|
101
|
+
RUBY
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Yield each direct child `Prism::Node` of `node` in `compact_child_nodes` order, without allocating an
|
|
105
|
+
# intermediate array. Nil / non-node input yields nothing (an optional field can be nil, so a caller may pass
|
|
106
|
+
# e.g. `node.body`). Pure and re-entrant; `break` / `next` / `return` in the block behave exactly as they
|
|
107
|
+
# would with `compact_child_nodes.each`. Walkers holding a known `Prism::Node` call `#rigor_each_child`
|
|
108
|
+
# directly; this wrapper is the nil-tolerant entry.
|
|
109
|
+
#
|
|
110
|
+
# @yieldparam child [Prism::Node]
|
|
111
|
+
def each_child(node, &)
|
|
112
|
+
node.rigor_each_child(&) if node.is_a?(Prism::Node)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "node_children"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Source
|
|
7
9
|
# Locates the deepest Prism AST node enclosing a given source position.
|
|
@@ -83,7 +85,7 @@ module Rigor
|
|
|
83
85
|
return nil unless node.is_a?(Prism::Node)
|
|
84
86
|
return nil unless contains?(node, offset)
|
|
85
87
|
|
|
86
|
-
node.
|
|
88
|
+
node.rigor_each_child do |child|
|
|
87
89
|
deeper = descend(child, offset)
|
|
88
90
|
return deeper if deeper
|
|
89
91
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "node_children"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Source
|
|
7
9
|
# Yields every `Prism::Node` reachable from a root in DFS pre-order.
|
|
@@ -28,7 +30,7 @@ module Rigor
|
|
|
28
30
|
return unless node.is_a?(Prism::Node)
|
|
29
31
|
|
|
30
32
|
yield node
|
|
31
|
-
node.
|
|
33
|
+
node.rigor_each_child { |child| walk(child, &) }
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Like {.each}, but also yields the node's lexical ancestor chain (outermost first, EXCLUDING the node
|
|
@@ -51,7 +53,7 @@ module Rigor
|
|
|
51
53
|
|
|
52
54
|
block.call(node, ancestors)
|
|
53
55
|
ancestors.push(node)
|
|
54
|
-
node.
|
|
56
|
+
node.rigor_each_child { |child| walk_with_ancestors(child, ancestors, &block) }
|
|
55
57
|
ancestors.pop
|
|
56
58
|
end
|
|
57
59
|
end
|
data/lib/rigor/source.rb
CHANGED
|
@@ -48,6 +48,30 @@ module Rigor
|
|
|
48
48
|
base.erase_to_rbs
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# True when `removed` is the *empty witness* of `base` — the single value whose removal makes this
|
|
52
|
+
# difference one of the catalogued `non-empty-*` / `non-zero-int` refinements (`""` for String, `0`
|
|
53
|
+
# for Integer, `[]` for Array, `{}` for Hash).
|
|
54
|
+
#
|
|
55
|
+
# This is the shared recogniser for the whole engine: the display side (`canonical_name` below),
|
|
56
|
+
# `ShapeDispatch`'s refinement-aware projections, and `MutationWidening`'s invalidation arm all ask
|
|
57
|
+
# this one question, so the recognised set cannot drift between them.
|
|
58
|
+
def removes_empty_witness?
|
|
59
|
+
return false unless base.is_a?(Nominal)
|
|
60
|
+
|
|
61
|
+
predicate = EMPTY_WITNESS_PREDICATES[base.class_name]
|
|
62
|
+
!!predicate && predicate.call(removed)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
EMPTY_WITNESS_PREDICATES = {
|
|
66
|
+
"String" => ->(removed) { removed.is_a?(Constant) && removed.value == "" },
|
|
67
|
+
"Integer" => lambda { |removed|
|
|
68
|
+
removed.is_a?(Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
69
|
+
},
|
|
70
|
+
"Array" => ->(removed) { removed.is_a?(Tuple) && removed.elements.empty? },
|
|
71
|
+
"Hash" => ->(removed) { removed.is_a?(HashShape) && removed.pairs.empty? }
|
|
72
|
+
}.freeze
|
|
73
|
+
private_constant :EMPTY_WITNESS_PREDICATES
|
|
74
|
+
|
|
51
75
|
def top
|
|
52
76
|
Trinary.no
|
|
53
77
|
end
|
|
@@ -80,47 +104,27 @@ module Rigor
|
|
|
80
104
|
# The recognised set is kept in sync with the imported-built-in catalogue
|
|
81
105
|
# ([`imported-built-in-types.md`](docs/type-specification/imported-built-in-types.md)).
|
|
82
106
|
def canonical_name
|
|
83
|
-
return nil unless
|
|
107
|
+
return nil unless removes_empty_witness?
|
|
84
108
|
|
|
85
|
-
send(CANONICAL_HANDLERS[base.class_name]
|
|
109
|
+
send(CANONICAL_HANDLERS[base.class_name])
|
|
86
110
|
end
|
|
87
111
|
|
|
88
112
|
CANONICAL_HANDLERS = {
|
|
89
113
|
"String" => :string_canonical_name,
|
|
90
114
|
"Integer" => :integer_canonical_name,
|
|
91
|
-
"Array" => :
|
|
92
|
-
"Hash" => :
|
|
115
|
+
"Array" => :array_canonical_name,
|
|
116
|
+
"Hash" => :hash_canonical_name
|
|
93
117
|
}.freeze
|
|
94
118
|
private_constant :CANONICAL_HANDLERS
|
|
95
119
|
|
|
96
|
-
def no_canonical_name
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
99
|
-
|
|
100
120
|
def string_canonical_name
|
|
101
|
-
return nil unless removed.is_a?(Constant) && removed.value == ""
|
|
102
|
-
|
|
103
121
|
"non-empty-string"
|
|
104
122
|
end
|
|
105
123
|
|
|
106
124
|
def integer_canonical_name
|
|
107
|
-
return nil unless removed.is_a?(Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
108
|
-
|
|
109
125
|
"non-zero-int"
|
|
110
126
|
end
|
|
111
127
|
|
|
112
|
-
def array_canonical_name_if_empty
|
|
113
|
-
return nil unless removed.is_a?(Tuple) && removed.elements.empty?
|
|
114
|
-
|
|
115
|
-
array_canonical_name
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def hash_canonical_name_if_empty
|
|
119
|
-
return nil unless removed.is_a?(HashShape) && removed.pairs.empty?
|
|
120
|
-
|
|
121
|
-
hash_canonical_name
|
|
122
|
-
end
|
|
123
|
-
|
|
124
128
|
def array_canonical_name
|
|
125
129
|
elem = base.type_args.first
|
|
126
130
|
return "non-empty-array" if elem.nil?
|
|
@@ -11,9 +11,12 @@ module Rigor
|
|
|
11
11
|
# inhabit the corresponding value types. RBS records correspond to the exact closed subset; Rigor
|
|
12
12
|
# extends that carrier with optional keys, read-only entry views, and an open/closed extra-key policy.
|
|
13
13
|
#
|
|
14
|
-
# Keys are restricted to
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# Keys are restricted to value-pinned scalar literals: Symbol, String, Integer, Float, and the
|
|
15
|
+
# `true` / `false` / `nil` singletons. Exact closed symbol-keyed shapes erase to the RBS record
|
|
16
|
+
# syntax `{ a: Integer, ?b: String }`; all other shapes degrade to `Hash[K, V]` or raw `Hash` when
|
|
17
|
+
# no useful bounds are available. Key identity follows Ruby's own `Hash` `eql?` semantics because
|
|
18
|
+
# the pairs are stored in a native Ruby Hash — `1` and `1.0` are DISTINCT keys (`1.eql?(1.0)` is
|
|
19
|
+
# false), while `1.0` and `1.00` are the same key.
|
|
17
20
|
#
|
|
18
21
|
# Equality and hashing are structural over the (key -> Rigor::Type) pair set and policy fields. Hash
|
|
19
22
|
# insertion order is preserved by the underlying storage but does NOT affect equality (matching
|
|
@@ -22,7 +25,7 @@ module Rigor
|
|
|
22
25
|
# See docs/type-specification/rbs-compatible-types.md (records) and
|
|
23
26
|
# docs/type-specification/rigor-extensions.md (hash shape).
|
|
24
27
|
class HashShape
|
|
25
|
-
ALLOWED_KEY_CLASSES = [Symbol, String].freeze
|
|
28
|
+
ALLOWED_KEY_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
26
29
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
27
30
|
POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
|
|
28
31
|
# A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
|
|
@@ -128,7 +131,8 @@ module Rigor
|
|
|
128
131
|
pairs.each_key do |key|
|
|
129
132
|
next if ALLOWED_KEY_CLASSES.any? { |klass| key.is_a?(klass) }
|
|
130
133
|
|
|
131
|
-
raise ArgumentError,
|
|
134
|
+
raise ArgumentError,
|
|
135
|
+
"HashShape keys must be one of #{ALLOWED_KEY_CLASSES.join(', ')}, got #{key.class}"
|
|
132
136
|
end
|
|
133
137
|
end
|
|
134
138
|
|
|
@@ -190,17 +194,28 @@ module Rigor
|
|
|
190
194
|
prefix = []
|
|
191
195
|
prefix << "readonly" if read_only_key?(key)
|
|
192
196
|
rendered_key = optional_key?(key) ? "?#{render_key(key)}" : render_key(key)
|
|
193
|
-
prefix << "#{rendered_key}
|
|
197
|
+
prefix << "#{rendered_key}#{key_separator(key)}"
|
|
194
198
|
"#{prefix.join(' ')} #{value.describe(verbosity)}"
|
|
195
199
|
end
|
|
196
200
|
|
|
197
201
|
# `describe` is a human-facing display contract (it feeds diagnostic messages, never a parser),
|
|
198
|
-
# so it keeps the compact `"a":` form for a quoted key.
|
|
199
|
-
#
|
|
202
|
+
# so it keeps the compact `"a":` form for a quoted key. Non-(Symbol|String) scalar keys render in
|
|
203
|
+
# the natural hashrocket spelling (`1 => 2`, `1.0 => 4`, `nil => 0`) via {#key_separator}; their
|
|
204
|
+
# `inspect` is the canonical Ruby literal, so describe stays deterministic. The RBS-erasure path
|
|
205
|
+
# uses {#erase_key_prefix} instead, which must emit a parseable key.
|
|
200
206
|
def render_key(key)
|
|
201
207
|
case key
|
|
202
208
|
when Symbol then key.to_s
|
|
203
|
-
|
|
209
|
+
else key.inspect
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Symbol / String keys keep the compact colon form (`a: 1`, `"k": 2`); every other scalar key uses
|
|
214
|
+
# the hashrocket (`1 => 2`) — the form the user would have to write in source.
|
|
215
|
+
def key_separator(key)
|
|
216
|
+
case key
|
|
217
|
+
when Symbol, String then ":"
|
|
218
|
+
else " =>"
|
|
204
219
|
end
|
|
205
220
|
end
|
|
206
221
|
|
|
@@ -230,8 +245,17 @@ module Rigor
|
|
|
230
245
|
"Hash[#{key_type.erase_to_rbs}, #{value_type.erase_to_rbs}]"
|
|
231
246
|
end
|
|
232
247
|
|
|
248
|
+
# Conservative per-key-class bound for the `Hash[K, V]` degradation. Symbol / String / Integer /
|
|
249
|
+
# Float keys widen to their class nominal; the `true` / `false` / `nil` singletons keep their
|
|
250
|
+
# literal carrier (the constant IS the class's whole value set, and RBS spells the literal — `nil`
|
|
251
|
+
# reads better than `NilClass`).
|
|
233
252
|
def hash_erasure_key_type
|
|
234
|
-
key_types = pairs.keys.map
|
|
253
|
+
key_types = pairs.keys.map do |key|
|
|
254
|
+
case key
|
|
255
|
+
when true, false, nil then Type::Combinator.constant_of(key)
|
|
256
|
+
else Type::Combinator.nominal_of(key.class)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
235
259
|
Type::Combinator.union(*key_types)
|
|
236
260
|
end
|
|
237
261
|
end
|
data/lib/rigor/version.rb
CHANGED
data/lib/rigortype.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Nothing in an application should ever load Rigor: it is a command-line tool
|
|
4
|
+
# that reads a project from the outside, not a library (ADR-27). This file
|
|
5
|
+
# exists only because `Bundler.require` — the default for every Gemfile entry,
|
|
6
|
+
# and what Rails runs at boot — requires a gem by its *gem* name, `rigortype`,
|
|
7
|
+
# while the library entry point is `rigor.rb`.
|
|
8
|
+
#
|
|
9
|
+
# Without this file that call raises a bare `LoadError` naming `rigortype`, so
|
|
10
|
+
# a `bundle add rigortype` succeeds and the *next* `rails s` dies with no
|
|
11
|
+
# explanation of what went wrong or what to do instead. The file is a guardrail,
|
|
12
|
+
# not an entry point: it warns and defines nothing. `require "rigor"` remains
|
|
13
|
+
# the (analyzer-internal) way in.
|
|
14
|
+
warn(<<~MESSAGE)
|
|
15
|
+
rigortype: `require "rigortype"` does nothing — Rigor is a command-line tool, not a library.
|
|
16
|
+
|
|
17
|
+
This usually means Rigor is in your Gemfile, where `Bundler.require` loads it at boot. Rigor
|
|
18
|
+
analyses your project from the outside and runs on its own Ruby, so a Gemfile entry constrains
|
|
19
|
+
your application's Ruby and dependency resolution without giving you anything. Remove it and
|
|
20
|
+
install Rigor independently: https://github.com/rigortype/rigor/blob/master/docs/install.md
|
|
21
|
+
|
|
22
|
+
To keep the Gemfile entry anyway, mark it `gem "rigortype", require: false` — that silences
|
|
23
|
+
this warning, but the resolution constraints remain.
|
|
24
|
+
MESSAGE
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "channel_index"
|
|
@@ -76,7 +78,7 @@ module Rigor
|
|
|
76
78
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
77
79
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
78
80
|
else
|
|
79
|
-
node.
|
|
81
|
+
node.rigor_each_child { |child| walk_for_channels(child, lexical_path, &) }
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
|
|
@@ -138,7 +140,7 @@ module Rigor
|
|
|
138
140
|
end
|
|
139
141
|
end
|
|
140
142
|
|
|
141
|
-
node.
|
|
143
|
+
node.rigor_each_child do |child|
|
|
142
144
|
names, dynamic = collect_stream_registrations(child, names: names, dynamic: dynamic)
|
|
143
145
|
end
|
|
144
146
|
[names, dynamic]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -97,7 +99,7 @@ module Rigor
|
|
|
97
99
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
98
100
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
99
101
|
else
|
|
100
|
-
node.
|
|
102
|
+
node.rigor_each_child { |child| walk_for_mailers(child, lexical_path, &) }
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
|
|
@@ -124,7 +126,7 @@ module Rigor
|
|
|
124
126
|
return [] if body.nil?
|
|
125
127
|
|
|
126
128
|
names = []
|
|
127
|
-
body.
|
|
129
|
+
body.rigor_each_child do |node|
|
|
128
130
|
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :include
|
|
129
131
|
|
|
130
132
|
(node.arguments&.arguments || []).each do |arg|
|
|
@@ -158,7 +160,7 @@ module Rigor
|
|
|
158
160
|
inner = local_name ? lexical_path + [local_name.delete_prefix("::")] : lexical_path
|
|
159
161
|
collect_module_actions(node.body, inner, accumulator) if node.body
|
|
160
162
|
else
|
|
161
|
-
node.
|
|
163
|
+
node.rigor_each_child { |child| collect_module_actions(child, lexical_path, accumulator) }
|
|
162
164
|
end
|
|
163
165
|
end
|
|
164
166
|
|
|
@@ -231,7 +233,7 @@ module Rigor
|
|
|
231
233
|
private_names = []
|
|
232
234
|
callback_names = []
|
|
233
235
|
|
|
234
|
-
body.
|
|
236
|
+
body.rigor_each_child do |node|
|
|
235
237
|
next unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
236
238
|
|
|
237
239
|
args = (node.arguments&.arguments || []).filter_map do |arg|
|
|
@@ -58,6 +58,12 @@ module Rigor
|
|
|
58
58
|
# receiver is `params.require(:symbol)`.
|
|
59
59
|
STRONG_PARAMS_RECEIVER_NAMES = %i[require permit_params strong_params].freeze
|
|
60
60
|
|
|
61
|
+
# Max edit distance between a non-column permit key and a real column for the key to count as a
|
|
62
|
+
# typo (and fire `unknown-permit-key`) rather than a legitimate virtual attribute. A tight bound
|
|
63
|
+
# keeps genuine typos (`emial`→`email`, distance 2) while never firing on a virtual attribute,
|
|
64
|
+
# whose name is nothing like any column.
|
|
65
|
+
PERMIT_KEY_TYPO_MAX_DISTANCE = 2
|
|
66
|
+
|
|
61
67
|
# One Action Pack observation. Carries no path — the caller (the `node_rule` block) positions
|
|
62
68
|
# it via `Plugin::Base#diagnostic`. `location` is the Prism location the diagnostic should point
|
|
63
69
|
# at (a call's `message_loc` for the call itself, or a sub-argument's location for
|
|
@@ -150,12 +156,11 @@ module Rigor
|
|
|
150
156
|
return [] if entry.nil? # unknown model — skip; the model lookup is best-effort.
|
|
151
157
|
|
|
152
158
|
columns = entry[:columns]
|
|
153
|
-
|
|
154
|
-
literal_permit_keys(call_node).map do |key_node, key_name|
|
|
159
|
+
literal_permit_keys(call_node).filter_map do |key_node, key_name|
|
|
155
160
|
if columns.include?(key_name)
|
|
156
161
|
permit_call_violation(call_node, model_class, key_name)
|
|
157
162
|
else
|
|
158
|
-
unknown_permit_key_violation(key_node, model_class, key_name,
|
|
163
|
+
unknown_permit_key_violation(key_node, model_class, key_name, columns)
|
|
159
164
|
end
|
|
160
165
|
end
|
|
161
166
|
end
|
|
@@ -347,13 +352,23 @@ module Rigor
|
|
|
347
352
|
)
|
|
348
353
|
end
|
|
349
354
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
355
|
+
# A permit key that is not a column is only a violation when it is a near-miss TYPO of a real
|
|
356
|
+
# column. Permitting a non-column is ordinary Rails: a Devise virtual attribute (`password`,
|
|
357
|
+
# `remember_me`, `otp_attempt`), a state-machine `*_event`, an `attr_accessor` setter, or a nested
|
|
358
|
+
# `*_attributes` key are all mass-assignable but absent from the schema. So fire only when a column
|
|
359
|
+
# sits within a tight edit distance of the key (a typo like `emial`→`email`); a key with no close
|
|
360
|
+
# column is presumed a deliberate virtual attribute and passes silently. Firing on every non-column
|
|
361
|
+
# key false-positives across the whole strong-params surface of a Devise/state-machine app.
|
|
362
|
+
def unknown_permit_key_violation(key_node, model_class, key_name, columns)
|
|
363
|
+
nearest = columns.min_by { |col| DidYouMean::Levenshtein.distance(key_name, col) }
|
|
364
|
+
return nil if nearest.nil?
|
|
365
|
+
return nil if DidYouMean::Levenshtein.distance(key_name, nearest) > PERMIT_KEY_TYPO_MAX_DISTANCE
|
|
366
|
+
|
|
354
367
|
Violation.new(
|
|
355
368
|
location: key_node.location,
|
|
356
|
-
message:
|
|
369
|
+
message: "Action Pack permit `#{key_name}` is not a column on `#{model_class}`. " \
|
|
370
|
+
"Did you mean `:#{nearest}`?",
|
|
371
|
+
severity: :error, rule: "unknown-permit-key"
|
|
357
372
|
)
|
|
358
373
|
end
|
|
359
374
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "controller_index"
|
|
@@ -89,7 +91,7 @@ module Rigor
|
|
|
89
91
|
|
|
90
92
|
walk_declarations(node.body, namespace: inner_namespace, &)
|
|
91
93
|
else
|
|
92
|
-
node.
|
|
94
|
+
node.rigor_each_child do |child|
|
|
93
95
|
walk_declarations(child, namespace: namespace, &)
|
|
94
96
|
end
|
|
95
97
|
end
|
|
@@ -142,7 +144,7 @@ module Rigor
|
|
|
142
144
|
return accumulator unless node.is_a?(Prism::Node)
|
|
143
145
|
|
|
144
146
|
accumulator << node.name if node.is_a?(Prism::DefNode) && node.receiver.nil?
|
|
145
|
-
node.
|
|
147
|
+
node.rigor_each_child { |child| collect_def_names(child, accumulator) }
|
|
146
148
|
accumulator
|
|
147
149
|
end
|
|
148
150
|
|
|
@@ -160,7 +162,7 @@ module Rigor
|
|
|
160
162
|
end
|
|
161
163
|
end
|
|
162
164
|
|
|
163
|
-
node.
|
|
165
|
+
node.rigor_each_child { |child| collect_include_targets(child, accumulator) }
|
|
164
166
|
accumulator
|
|
165
167
|
end
|
|
166
168
|
|
|
@@ -182,6 +182,27 @@ module Rigor
|
|
|
182
182
|
Rigor::Type::Combinator.nominal_of(class_name)
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
+
# Phase 5b (2026-07-10) — keep the strong-parameters fluent chain typed. `params` types to
|
|
186
|
+
# `ActionController::Parameters` (above), but the chained `params.require(:user).permit(:name)` /
|
|
187
|
+
# `params.permit(...)` calls returned `Dynamic` at the first hop (Parameters ships no bundled RBS,
|
|
188
|
+
# so a call on it resolves lenient-to-Dynamic), leaking every downstream site (`.permit`, `.to_h`,
|
|
189
|
+
# `.each`) to unprotected. These three methods return another `Parameters`, so gate on a
|
|
190
|
+
# `Parameters` receiver and re-type the result as the same lenient nominal — the chain stays a
|
|
191
|
+
# concrete receiver end-to-end and `coverage --protection` counts the sites as protected.
|
|
192
|
+
#
|
|
193
|
+
# `require` may return a scalar for a flat key at runtime (`params.require(:id) → String`), but
|
|
194
|
+
# typing it as the RBS-less `Parameters` is FP-safe by the same argument as the readers: every
|
|
195
|
+
# method on a Parameters value stays engine-lenient (no `undefined-method`), and this types the
|
|
196
|
+
# container only, never a caller's argument (ADR-5). The GitLab strong-params survey (108 leaked
|
|
197
|
+
# `.permit` sites) is the demand.
|
|
198
|
+
STRONG_PARAMS_CHAIN_METHODS = %i[require permit permit!].freeze
|
|
199
|
+
|
|
200
|
+
dynamic_return receivers: [REQUEST_CONTEXT_READER_TYPES[:params]], methods: STRONG_PARAMS_CHAIN_METHODS do |call_node, _scope|
|
|
201
|
+
next nil unless call_node.is_a?(Prism::CallNode)
|
|
202
|
+
|
|
203
|
+
Rigor::Type::Combinator.nominal_of(REQUEST_CONTEXT_READER_TYPES[:params])
|
|
204
|
+
end
|
|
205
|
+
|
|
185
206
|
private
|
|
186
207
|
|
|
187
208
|
# True when the current `self` is a controller — the enclosing class is one the discoverer
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "job_index"
|
|
@@ -66,7 +68,7 @@ module Rigor
|
|
|
66
68
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
67
69
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
68
70
|
else
|
|
69
|
-
node.
|
|
71
|
+
node.rigor_each_child { |child| walk_for_jobs(child, lexical_path, &) }
|
|
70
72
|
end
|
|
71
73
|
end
|
|
72
74
|
|
|
@@ -118,7 +120,7 @@ module Rigor
|
|
|
118
120
|
def lookup_perform_def(body)
|
|
119
121
|
return nil if body.nil?
|
|
120
122
|
|
|
121
|
-
body.
|
|
123
|
+
body.rigor_each_child do |node|
|
|
122
124
|
next unless node.is_a?(Prism::DefNode) && node.name == :perform
|
|
123
125
|
next if node.receiver.is_a?(Prism::SelfNode)
|
|
124
126
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -50,7 +52,7 @@ module Rigor
|
|
|
50
52
|
return if node.nil?
|
|
51
53
|
|
|
52
54
|
yield node
|
|
53
|
-
node.
|
|
55
|
+
node.rigor_each_child { |child| walk(child, &) }
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def visit_call(node)
|
|
@@ -90,7 +92,7 @@ module Rigor
|
|
|
90
92
|
# than firing a false `unknown-column` against every key.
|
|
91
93
|
return push_recognised(node, entry, keyword_pairs.map { |p| p[:key] }) if entry.column_names.empty?
|
|
92
94
|
|
|
93
|
-
unknown = keyword_pairs.reject { |pair| valid_query_key?(entry, pair[:key]) }
|
|
95
|
+
unknown = keyword_pairs.reject { |pair| nested_condition?(pair[:value]) || valid_query_key?(entry, pair[:key]) }
|
|
94
96
|
if unknown.empty?
|
|
95
97
|
keyword_pairs.each { |pair| validate_enum_value(node, entry, pair) }
|
|
96
98
|
push_recognised(node, entry, keyword_pairs.map { |p| p[:key] })
|
|
@@ -129,6 +131,15 @@ module Rigor
|
|
|
129
131
|
assoc && assoc[:kind] == :singular
|
|
130
132
|
end
|
|
131
133
|
|
|
134
|
+
# A keyword pair with a Hash value is a nested / joined-table condition (`Approval.where(approvals:
|
|
135
|
+
# { user_id: 1 })`, `where(users: { name: 'x' })`), where the key names an association or a joined
|
|
136
|
+
# table, not a column on the receiver. Rails resolves it through the join, so it must not be checked
|
|
137
|
+
# against the receiver's own columns. (An Array value — `where(status: [:a, :b])` — is an IN query on
|
|
138
|
+
# a real column, not nested, so only a Hash is skipped.)
|
|
139
|
+
def nested_condition?(value_node)
|
|
140
|
+
value_node.is_a?(Prism::HashNode) || value_node.is_a?(Prism::KeywordHashNode)
|
|
141
|
+
end
|
|
142
|
+
|
|
132
143
|
# When the column is an enum-bearing column AND the passed value is a Symbol literal, the value
|
|
133
144
|
# MUST be one of the declared enum values. Non-Symbol values (variables, expressions) decline so
|
|
134
145
|
# dynamic call sites stay silent. Sequences (`status: [:active, :archived]`) are intentionally NOT
|