rigortype 0.1.9 → 0.1.11
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 +1 -1
- data/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +57 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli.rb +88 -5
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +159 -4
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +143 -12
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +115 -7
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +26 -2
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +28 -5
- data/lib/rigor/plugin/manifest.rb +33 -5
- data/lib/rigor/plugin/registry.rb +21 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +178 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +310 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +76 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +177 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +589 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +150 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +123 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +247 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +273 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +240 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +94 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +514 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +34 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +463 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +277 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +167 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +161 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +103 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +490 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +158 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +180 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "type_translator"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Sorbet < Rigor::Plugin::Base
|
|
10
|
+
# Slice 6 of ADR-11 — recognises `T.absurd(x)` calls and
|
|
11
|
+
# composes them with the engine's flow-sensitive
|
|
12
|
+
# narrowing. `T.absurd` asserts that a code branch is
|
|
13
|
+
# statically unreachable; it's the standard Sorbet idiom
|
|
14
|
+
# for case/when exhaustiveness:
|
|
15
|
+
#
|
|
16
|
+
# case x
|
|
17
|
+
# when A then ...
|
|
18
|
+
# when B then ...
|
|
19
|
+
# else
|
|
20
|
+
# T.absurd(x)
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# If every case has been handled, `x` at the `else` branch
|
|
24
|
+
# has been narrowed to `T.noreturn` (Rigor's `Type::Bot`)
|
|
25
|
+
# and the assertion holds. If the user forgot a case, `x`
|
|
26
|
+
# narrows to whatever's left and the assertion is wrong —
|
|
27
|
+
# we surface that mistake as `plugin.sorbet.absurd-reachable`.
|
|
28
|
+
#
|
|
29
|
+
# ## Two-phase mechanism
|
|
30
|
+
#
|
|
31
|
+
# The recogniser is invoked from `flow_contribution_for`
|
|
32
|
+
# where the per-node `scope:` carries the proper narrowing
|
|
33
|
+
# context. It returns:
|
|
34
|
+
#
|
|
35
|
+
# - A `FlowContribution` with `return_type: bot` and
|
|
36
|
+
# `exceptional: :raises` regardless of reachability
|
|
37
|
+
# (faithful to `T.absurd`'s runtime behaviour: it always
|
|
38
|
+
# raises). This lets the engine's existing flow analysis
|
|
39
|
+
# treat code after `T.absurd` as unreachable, matching
|
|
40
|
+
# what users of Sorbet expect.
|
|
41
|
+
# - When the branch is REACHABLE (the discriminant's type
|
|
42
|
+
# isn't `bot`), the recogniser also records the call
|
|
43
|
+
# node in a per-plugin set. The plugin's
|
|
44
|
+
# `diagnostics_for_file` later walks the AST for
|
|
45
|
+
# `T.absurd` calls and emits a
|
|
46
|
+
# `plugin.sorbet.absurd-reachable` warning at every
|
|
47
|
+
# call_node whose object identity matches the recorded
|
|
48
|
+
# set. We rely on the runner only parsing each file
|
|
49
|
+
# once per run, so the same Prism node object is seen
|
|
50
|
+
# in both `flow_contribution_for` and
|
|
51
|
+
# `diagnostics_for_file`.
|
|
52
|
+
module AbsurdRecognizer
|
|
53
|
+
# @param call_node [Prism::CallNode]
|
|
54
|
+
# @return [Boolean] true when `call_node` is `T.absurd(x)`.
|
|
55
|
+
def self.absurd_call?(call_node)
|
|
56
|
+
return false unless call_node.is_a?(Prism::CallNode)
|
|
57
|
+
return false unless call_node.name == :absurd
|
|
58
|
+
return false unless TypeTranslator.sorbet_t_namespaced?(call_node.receiver)
|
|
59
|
+
|
|
60
|
+
# Slice 6 only handles single-argument `T.absurd(x)`;
|
|
61
|
+
# no-arg / multi-arg shapes are syntax errors at
|
|
62
|
+
# Sorbet's level too.
|
|
63
|
+
arguments = call_node.arguments&.arguments
|
|
64
|
+
arguments&.size == 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param call_node [Prism::CallNode]
|
|
68
|
+
# @param scope [Rigor::Scope, nil]
|
|
69
|
+
# @return [Boolean] true when the discriminant has been
|
|
70
|
+
# narrowed to `bot` (the branch is unreachable, so
|
|
71
|
+
# `T.absurd` is correct). The caller suppresses the
|
|
72
|
+
# `absurd-reachable` diagnostic in this case.
|
|
73
|
+
def self.exhaustive?(call_node, scope)
|
|
74
|
+
return false if scope.nil?
|
|
75
|
+
|
|
76
|
+
arg = call_node.arguments.arguments.first
|
|
77
|
+
arg_type = scope.type_of(arg)
|
|
78
|
+
arg_type.equal?(Rigor::Type::Bot.instance) || arg_type.is_a?(Rigor::Type::Bot)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
# On synthetic / unrecognised nodes the typer may
|
|
81
|
+
# raise; treat as "can't prove unreachable" so the
|
|
82
|
+
# diagnostic fires conservatively.
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The contribution every `T.absurd` call gets,
|
|
87
|
+
# regardless of static reachability — `T.absurd` raises
|
|
88
|
+
# at runtime, so its return type is `bot` and the call
|
|
89
|
+
# is exceptional. This lets the engine's flow analysis
|
|
90
|
+
# treat code after the call as unreachable (no
|
|
91
|
+
# `flow.unreachable-branch` from us; that's an engine
|
|
92
|
+
# rule that consults the same effect lattice).
|
|
93
|
+
def self.contribution(call_node, plugin_id)
|
|
94
|
+
Rigor::FlowContribution.new(
|
|
95
|
+
return_type: Rigor::Type::Combinator.bot,
|
|
96
|
+
exceptional: :raises,
|
|
97
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
98
|
+
source_family: "plugin.#{plugin_id}",
|
|
99
|
+
plugin_id: plugin_id,
|
|
100
|
+
node: call_node,
|
|
101
|
+
descriptor: nil
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "type_translator"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Sorbet < Rigor::Plugin::Base
|
|
10
|
+
# Lifts Sorbet's type-assertion calls (`T.let`, `T.cast`,
|
|
11
|
+
# `T.must`, `T.must_because`, `T.unsafe`, `T.reveal_type`)
|
|
12
|
+
# into `FlowContribution` return-type contributions.
|
|
13
|
+
# ADR-11 slice 2 covered the original four; the
|
|
14
|
+
# `must_because` / `reveal_type` light follow-up extends
|
|
15
|
+
# the same module rather than splitting into a parallel
|
|
16
|
+
# recogniser.
|
|
17
|
+
#
|
|
18
|
+
# | Sorbet form | Contribution |
|
|
19
|
+
# | ---------------------------- | --------------------------------------- |
|
|
20
|
+
# | `T.let(expr, T)` | return type ← translated `T` |
|
|
21
|
+
# | `T.cast(expr, T)` | return type ← translated `T` |
|
|
22
|
+
# | `T.must(expr)` | return type ← `inferred(expr) - nil` |
|
|
23
|
+
# | `T.must_because(expr, "..")` | return type ← `inferred(expr) - nil` |
|
|
24
|
+
# | `T.unsafe(x)` | return type ← `Dynamic[top]` |
|
|
25
|
+
# | `T.reveal_type(expr)` | return type ← `inferred(expr)` (passes through) |
|
|
26
|
+
# | `T.assert_type!(expr, T)` | return type ← translated `T` + static subtype check |
|
|
27
|
+
# | `T.bind(self, T)` | return type ← `Constant[nil]` + post_return_fact narrowing self to translated `T` |
|
|
28
|
+
#
|
|
29
|
+
# The Sorbet runtime's `T.let` / `T.cast` actually return
|
|
30
|
+
# the inner expression unchanged at runtime; their job is
|
|
31
|
+
# purely to *assert* a static type. From Rigor's static
|
|
32
|
+
# perspective the simplest faithful translation is "the
|
|
33
|
+
# call's return type IS the asserted type" — the call
|
|
34
|
+
# site's downstream uses see that type. This matches what
|
|
35
|
+
# `%a{rigor:v1:assert: x is T}` would do for an assignment
|
|
36
|
+
# in the surrounding scope.
|
|
37
|
+
#
|
|
38
|
+
# `T.must_because` is `T.must` with a second-argument
|
|
39
|
+
# string explanation. The static behaviour is identical to
|
|
40
|
+
# `T.must` — strip `nil` from the inferred type — so the
|
|
41
|
+
# recogniser dispatches through the same path.
|
|
42
|
+
#
|
|
43
|
+
# `T.reveal_type` is "diagnostic-only" in Sorbet: it
|
|
44
|
+
# passes the value through unchanged at runtime AND
|
|
45
|
+
# surfaces the inferred static type as a build-time
|
|
46
|
+
# message. The recogniser contributes the inferred type
|
|
47
|
+
# (so chained call sites still resolve as if the
|
|
48
|
+
# `T.reveal_type` wrapper weren't there); the plugin's
|
|
49
|
+
# `diagnostics_for_file` hook surfaces the
|
|
50
|
+
# `plugin.sorbet.reveal-type` `:info` message for human
|
|
51
|
+
# consumption.
|
|
52
|
+
#
|
|
53
|
+
# `T.bind(self, T)` is recognised as block-scope self
|
|
54
|
+
# narrowing. The recogniser returns a contribution whose
|
|
55
|
+
# `post_return_facts` carries a `Fact(target_kind: :self)`
|
|
56
|
+
# so the engine's `apply_self_post_return_fact` narrows
|
|
57
|
+
# `scope.self_type` for the surrounding scope (in a block
|
|
58
|
+
# body, the rest of the block). The first argument MUST be
|
|
59
|
+
# a literal `Prism::SelfNode` — Sorbet rejects other
|
|
60
|
+
# receivers and the recogniser mirrors that. The runtime
|
|
61
|
+
# call returns nil, so the static return type is
|
|
62
|
+
# `Constant[nil]`.
|
|
63
|
+
module AssertionRecognizer
|
|
64
|
+
# Method names this recogniser claims as Sorbet
|
|
65
|
+
# assertions. The plugin checks call sites against this
|
|
66
|
+
# set before any catalog lookup so a `T.let` call
|
|
67
|
+
# inside an analysed file always resolves through this
|
|
68
|
+
# module.
|
|
69
|
+
SORBET_ASSERTIONS = %i[let cast must must_because unsafe reveal_type assert_type! bind].freeze
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
# @param call_node [Prism::CallNode]
|
|
74
|
+
# @param scope [Rigor::Scope]
|
|
75
|
+
# @param plugin_id [String] used for the contribution's
|
|
76
|
+
# `provenance.source_family`.
|
|
77
|
+
# @return [Rigor::FlowContribution, nil]
|
|
78
|
+
def recognize(call_node:, scope:, plugin_id:)
|
|
79
|
+
return nil unless TypeTranslator.sorbet_t_namespaced?(call_node.receiver)
|
|
80
|
+
return nil unless SORBET_ASSERTIONS.include?(call_node.name)
|
|
81
|
+
|
|
82
|
+
return recognize_bind(call_node, plugin_id) if call_node.name == :bind
|
|
83
|
+
|
|
84
|
+
return_type = return_type_for(call_node, scope)
|
|
85
|
+
return nil if return_type.nil?
|
|
86
|
+
|
|
87
|
+
contribution(call_node, return_type, plugin_id)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def return_type_for(call_node, scope)
|
|
91
|
+
case call_node.name
|
|
92
|
+
when :let, :cast then resolve_typed_assertion(call_node)
|
|
93
|
+
when :must, :must_because then resolve_must(call_node, scope)
|
|
94
|
+
when :unsafe then Rigor::Type::Combinator.untyped
|
|
95
|
+
when :reveal_type then resolve_reveal_type(call_node, scope)
|
|
96
|
+
when :assert_type! then resolve_typed_assertion(call_node)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `T.bind(self, T)` recognition. Sorbet rejects any
|
|
101
|
+
# non-`self` receiver argument, and the recogniser
|
|
102
|
+
# mirrors that — calls like `T.bind(other, X)` fall
|
|
103
|
+
# through silently. The contribution carries:
|
|
104
|
+
#
|
|
105
|
+
# - `return_type: Constant[nil]` — Sorbet's runtime
|
|
106
|
+
# `T.bind` returns nil; chained calls would be a
|
|
107
|
+
# bug, but the typing stays accurate.
|
|
108
|
+
# - `post_return_facts: [Fact(target_kind: :self,
|
|
109
|
+
# type: T)]` — the engine's
|
|
110
|
+
# `apply_self_post_return_fact` narrows
|
|
111
|
+
# `scope.self_type` for the surrounding scope. In a
|
|
112
|
+
# block body, that scope is the block's own, so the
|
|
113
|
+
# narrowing applies to the rest of the block —
|
|
114
|
+
# matching Sorbet's documented contract.
|
|
115
|
+
def recognize_bind(call_node, plugin_id)
|
|
116
|
+
first_arg = nth_argument(call_node, 0)
|
|
117
|
+
return nil unless first_arg.is_a?(Prism::SelfNode)
|
|
118
|
+
|
|
119
|
+
type_arg = nth_argument(call_node, 1)
|
|
120
|
+
return nil if type_arg.nil?
|
|
121
|
+
|
|
122
|
+
asserted = TypeTranslator.translate(type_arg)
|
|
123
|
+
return nil if asserted.nil?
|
|
124
|
+
|
|
125
|
+
fact = Rigor::FlowContribution::Fact.new(
|
|
126
|
+
target_kind: :self, target_name: :self, type: asserted
|
|
127
|
+
)
|
|
128
|
+
Rigor::FlowContribution.new(
|
|
129
|
+
return_type: Rigor::Type::Combinator.constant_of(nil),
|
|
130
|
+
post_return_facts: [fact],
|
|
131
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
132
|
+
source_family: "plugin.#{plugin_id}",
|
|
133
|
+
plugin_id: plugin_id,
|
|
134
|
+
node: call_node,
|
|
135
|
+
descriptor: nil
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `T.assert_type!(expr, T)` shares the typed-assertion
|
|
141
|
+
# contribution shape with `T.cast` (return is the
|
|
142
|
+
# asserted type), so the recogniser delegates the
|
|
143
|
+
# return-type half through `resolve_typed_assertion`.
|
|
144
|
+
# The static subtype check that distinguishes
|
|
145
|
+
# `assert_type!` from `cast` lives in the plugin's
|
|
146
|
+
# `diagnostics_for_file` hook (mirroring the
|
|
147
|
+
# absurd-recognizer pattern: record the call here, emit
|
|
148
|
+
# the diagnostic from the per-file walker).
|
|
149
|
+
def assert_type_check(call_node, scope)
|
|
150
|
+
return nil if scope.nil?
|
|
151
|
+
|
|
152
|
+
inner = nth_argument(call_node, 0)
|
|
153
|
+
asserted_node = nth_argument(call_node, 1)
|
|
154
|
+
return nil if inner.nil? || asserted_node.nil?
|
|
155
|
+
|
|
156
|
+
asserted_type = TypeTranslator.translate(asserted_node)
|
|
157
|
+
return nil if asserted_type.nil?
|
|
158
|
+
|
|
159
|
+
inferred = scope.type_of(inner)
|
|
160
|
+
[inferred, asserted_type]
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# `T.reveal_type(expr)` returns `expr` unchanged at
|
|
166
|
+
# runtime; the Sorbet-side semantics is "make the
|
|
167
|
+
# inferred static type visible to the user." The
|
|
168
|
+
# contribution mirrors `T.must` minus the nil-stripping:
|
|
169
|
+
# the call's return type is the inner expression's
|
|
170
|
+
# inferred type. The companion diagnostic is emitted by
|
|
171
|
+
# the plugin's `diagnostics_for_file` hook through
|
|
172
|
+
# {RevealTypeRecognizer}; the recogniser here is
|
|
173
|
+
# contribution-only.
|
|
174
|
+
def resolve_reveal_type(call_node, scope)
|
|
175
|
+
inner = nth_argument(call_node, 0)
|
|
176
|
+
return Rigor::Type::Combinator.untyped if inner.nil? || scope.nil?
|
|
177
|
+
|
|
178
|
+
inner_type = scope.type_of(inner)
|
|
179
|
+
inner_type || Rigor::Type::Combinator.untyped
|
|
180
|
+
rescue StandardError
|
|
181
|
+
# Synthetic / virtual nodes can raise from
|
|
182
|
+
# `scope.type_of`; degrade gracefully so the dispatcher
|
|
183
|
+
# can still proceed with a benign untyped envelope.
|
|
184
|
+
Rigor::Type::Combinator.untyped
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# `T.let(expr, T)` and `T.cast(expr, T)` share the same
|
|
188
|
+
# 2-argument shape: `arguments[1]` is the type
|
|
189
|
+
# expression. The first argument is opaque to slice 2 —
|
|
190
|
+
# we don't try to verify it at runtime; that's `srb tc`'s
|
|
191
|
+
# job and is out of scope per ADR-11.
|
|
192
|
+
def resolve_typed_assertion(call_node)
|
|
193
|
+
type_arg = nth_argument(call_node, 1)
|
|
194
|
+
return nil if type_arg.nil?
|
|
195
|
+
|
|
196
|
+
TypeTranslator.translate(type_arg)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# `T.must(expr)` strips `nil` from `expr`'s inferred
|
|
200
|
+
# type. The call's target type is therefore
|
|
201
|
+
# `inferred(expr) - Constant[nil]`. Falls back to the
|
|
202
|
+
# untyped envelope when the inferred shape is itself
|
|
203
|
+
# `Dynamic[top]` or when no scope is available
|
|
204
|
+
# (synthetic / virtual-node call sites).
|
|
205
|
+
def resolve_must(call_node, scope)
|
|
206
|
+
inner = nth_argument(call_node, 0)
|
|
207
|
+
return nil if inner.nil? || scope.nil?
|
|
208
|
+
|
|
209
|
+
inner_type = scope.type_of(inner)
|
|
210
|
+
return Rigor::Type::Combinator.untyped if inner_type.nil?
|
|
211
|
+
|
|
212
|
+
strip_nil(inner_type)
|
|
213
|
+
rescue StandardError
|
|
214
|
+
# `scope.type_of` may raise on synthetic nodes; degrade
|
|
215
|
+
# to "no contribution" rather than crash the dispatcher.
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Removes `nil` (`Constant[nil]`) from `type` using the
|
|
220
|
+
# `Difference` carrier. Idempotent on shapes that don't
|
|
221
|
+
# contain nil — the resulting `Difference[base, removed]`
|
|
222
|
+
# collapses to `base` if `base` already excludes the
|
|
223
|
+
# removed value, but the simple form here is good enough
|
|
224
|
+
# for slice 2; the precise normalisation lands when the
|
|
225
|
+
# Difference carrier gets full algebraic support.
|
|
226
|
+
def strip_nil(type)
|
|
227
|
+
Rigor::Type::Combinator.difference(
|
|
228
|
+
type, Rigor::Type::Combinator.constant_of(nil)
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def nth_argument(call_node, index)
|
|
233
|
+
call_node.arguments&.arguments&.[](index)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def contribution(call_node, return_type, plugin_id)
|
|
237
|
+
Rigor::FlowContribution.new(
|
|
238
|
+
return_type: return_type,
|
|
239
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
240
|
+
source_family: "plugin.#{plugin_id}",
|
|
241
|
+
plugin_id: plugin_id,
|
|
242
|
+
node: call_node,
|
|
243
|
+
descriptor: nil
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Sorbet < Rigor::Plugin::Base
|
|
6
|
+
# Per-run table of method signatures keyed by the
|
|
7
|
+
# `(class_name, method_name, kind)` triple. Built by
|
|
8
|
+
# {CatalogWalker} during the plugin's lazy pre-walk; read
|
|
9
|
+
# by {Sorbet#flow_contribution_for} at every call site.
|
|
10
|
+
#
|
|
11
|
+
# The catalog is mutable while it is being built, then
|
|
12
|
+
# frozen via {#freeze!} before the first read. Construction
|
|
13
|
+
# mutability is intentional — slice 1 builds the catalog
|
|
14
|
+
# incrementally as the walker visits each project file —
|
|
15
|
+
# but consumers MUST treat the catalog as read-only.
|
|
16
|
+
class Catalog
|
|
17
|
+
# Frozen empty bucket reused for classes that have no
|
|
18
|
+
# recorded mixins. Avoids allocating a fresh Hash on
|
|
19
|
+
# every `mixins_for` query.
|
|
20
|
+
EMPTY_MIXINS = { include: [].freeze, extend: [].freeze }.freeze
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@entries = {}
|
|
24
|
+
# ADR-11 slice 8 — per-class mixin declarations
|
|
25
|
+
# collected by `CatalogWalker`. Lookup-time chain
|
|
26
|
+
# traversal lifts sigs declared on a mixed-in
|
|
27
|
+
# module to call sites on the host class.
|
|
28
|
+
@mixins = {}
|
|
29
|
+
@frozen_after_build = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param signature [MethodSignature]
|
|
33
|
+
def record(signature)
|
|
34
|
+
raise "Catalog already finalised" if @frozen_after_build
|
|
35
|
+
|
|
36
|
+
key = key_for(signature.class_name, signature.method_name, signature.kind)
|
|
37
|
+
@entries[key] = signature
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param class_name [String] the class / module that
|
|
41
|
+
# carries the mixin (`class Post; include Foo; end`
|
|
42
|
+
# records under `"Post"`).
|
|
43
|
+
# @param kind [:include, :extend]
|
|
44
|
+
# @param module_name [String] the textual name of the
|
|
45
|
+
# mixed-in module as it appeared at the include /
|
|
46
|
+
# extend site (`"Foo"`, `"Foo::Bar"`, `"::Foo"`).
|
|
47
|
+
def record_mixin(class_name:, kind:, module_name:)
|
|
48
|
+
raise "Catalog already finalised" if @frozen_after_build
|
|
49
|
+
|
|
50
|
+
bucket = (@mixins[class_name] ||= { include: [], extend: [] })
|
|
51
|
+
list = bucket[kind]
|
|
52
|
+
list << module_name unless list.include?(module_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def freeze!
|
|
56
|
+
@frozen_after_build = true
|
|
57
|
+
@entries.freeze
|
|
58
|
+
@mixins.each_value do |bucket|
|
|
59
|
+
bucket.each_value(&:freeze)
|
|
60
|
+
bucket.freeze
|
|
61
|
+
end
|
|
62
|
+
@mixins.freeze
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [MethodSignature, nil]
|
|
67
|
+
def lookup(class_name:, method_name:, kind:)
|
|
68
|
+
@entries[key_for(class_name, method_name, kind)]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param class_name [String]
|
|
72
|
+
# @return [Hash{Symbol => Array<String>}] frozen mapping
|
|
73
|
+
# `{ include: [...], extend: [...] }`. Returns
|
|
74
|
+
# {EMPTY_MIXINS} when no mixins were recorded.
|
|
75
|
+
def mixins_for(class_name)
|
|
76
|
+
@mixins[class_name] || EMPTY_MIXINS
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def empty?
|
|
80
|
+
@entries.empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def size
|
|
84
|
+
@entries.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def key_for(class_name, method_name, kind)
|
|
90
|
+
[class_name.to_s, method_name.to_sym, kind]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "method_signature"
|
|
6
|
+
require_relative "sig_parser"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Plugin
|
|
10
|
+
class Sorbet < Rigor::Plugin::Base
|
|
11
|
+
# Walks a parsed Prism program looking for
|
|
12
|
+
# `sig { ... }` / `sig do ... end` calls that immediately
|
|
13
|
+
# precede a `def` (or a `def self.foo` / `class << self;
|
|
14
|
+
# def foo; end`). Each recognised pair is parsed by
|
|
15
|
+
# {SigParser} and recorded in the {Catalog} under its
|
|
16
|
+
# qualified `(class_name, method_name, kind)` key.
|
|
17
|
+
#
|
|
18
|
+
# Anything we don't recognise (a stray `sig { ... }` not
|
|
19
|
+
# followed by a `def`, a `def` with no preceding `sig`,
|
|
20
|
+
# malformed sig blocks, etc.) is reported back to the
|
|
21
|
+
# caller through `parse_errors:` so the plugin can emit a
|
|
22
|
+
# `plugin.sorbet.parse-error` diagnostic. Walking is
|
|
23
|
+
# otherwise infallible — a bad sig block does not abort
|
|
24
|
+
# the catalog build for the rest of the file.
|
|
25
|
+
module CatalogWalker
|
|
26
|
+
# Detected error during walking. `kind` is one of:
|
|
27
|
+
# `:no_block` / `:empty_block` / `:missing_returns_or_void`
|
|
28
|
+
# / `:duplicate_sig` / `:dangling_sig`.
|
|
29
|
+
ParseError = Data.define(:kind, :node, :path)
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @param root [Prism::Node] the file's program node.
|
|
34
|
+
# @param catalog [Catalog] mutable; signatures are
|
|
35
|
+
# recorded into it.
|
|
36
|
+
# @param path [String] file path used for diagnostic
|
|
37
|
+
# provenance.
|
|
38
|
+
# @return [Array<ParseError>] errors observed during the
|
|
39
|
+
# walk; empty when the file is sig-clean.
|
|
40
|
+
def walk(root:, catalog:, path:)
|
|
41
|
+
state = State.new(catalog: catalog, path: path)
|
|
42
|
+
walk_node(root, state, lexical_path: [], in_singleton_class: false)
|
|
43
|
+
state.errors
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
State = Struct.new(:catalog, :path, :errors, keyword_init: true) do
|
|
47
|
+
def initialize(catalog:, path:)
|
|
48
|
+
super(catalog: catalog, path: path, errors: [])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def record_error(kind, node)
|
|
52
|
+
errors << ParseError.new(kind: kind, node: node, path: path)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def walk_node(node, state, lexical_path:, in_singleton_class:)
|
|
57
|
+
return unless node.is_a?(Prism::Node)
|
|
58
|
+
|
|
59
|
+
case node
|
|
60
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
61
|
+
descend_class_or_module(node, state, lexical_path)
|
|
62
|
+
when Prism::SingletonClassNode
|
|
63
|
+
descend_singleton_class(node, state, lexical_path)
|
|
64
|
+
when Prism::StatementsNode
|
|
65
|
+
walk_statements(node, state, lexical_path: lexical_path, in_singleton_class: in_singleton_class)
|
|
66
|
+
when Prism::DefNode
|
|
67
|
+
# A `def` not preceded by a `sig` is fine; we just
|
|
68
|
+
# don't record anything for it. The interesting case
|
|
69
|
+
# is in `walk_statements`, which pairs sig+def.
|
|
70
|
+
else
|
|
71
|
+
node.compact_child_nodes.each do |child|
|
|
72
|
+
walk_node(child, state, lexical_path: lexical_path, in_singleton_class: in_singleton_class)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def descend_class_or_module(node, state, lexical_path)
|
|
78
|
+
name = qualified_name_for(node.constant_path)
|
|
79
|
+
if name && node.body
|
|
80
|
+
child_prefix = lexical_path + [name]
|
|
81
|
+
walk_node(node.body, state, lexical_path: child_prefix, in_singleton_class: false)
|
|
82
|
+
elsif node.body
|
|
83
|
+
walk_node(node.body, state, lexical_path: lexical_path, in_singleton_class: false)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def descend_singleton_class(node, state, lexical_path)
|
|
88
|
+
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
89
|
+
walk_node(node.body, state, lexical_path: lexical_path, in_singleton_class: true)
|
|
90
|
+
elsif node.body
|
|
91
|
+
walk_node(node.body, state, lexical_path: lexical_path, in_singleton_class: false)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The pair-finding loop. Walks a `StatementsNode`'s
|
|
96
|
+
# children left-to-right; when it encounters a `sig`
|
|
97
|
+
# call, it remembers it and consumes the very next
|
|
98
|
+
# `def` / `def self.foo` as the target. Anything between
|
|
99
|
+
# a sig and its def (a comment is fine — comments aren't
|
|
100
|
+
# AST nodes — but a method call would be a problem)
|
|
101
|
+
# leaves the sig dangling.
|
|
102
|
+
def walk_statements(statements, state, lexical_path:, in_singleton_class:)
|
|
103
|
+
pending_sig = nil
|
|
104
|
+
|
|
105
|
+
statements.body.each do |child|
|
|
106
|
+
if pending_sig && def_node?(child)
|
|
107
|
+
record_def_with_sig(child, pending_sig, state, lexical_path, in_singleton_class)
|
|
108
|
+
pending_sig = nil
|
|
109
|
+
elsif sig_call?(child)
|
|
110
|
+
state.record_error(:duplicate_sig, pending_sig) if pending_sig
|
|
111
|
+
pending_sig = child
|
|
112
|
+
else
|
|
113
|
+
if pending_sig
|
|
114
|
+
state.record_error(:dangling_sig, pending_sig)
|
|
115
|
+
pending_sig = nil
|
|
116
|
+
end
|
|
117
|
+
# ADR-11 slice 8 — record `include` / `extend`
|
|
118
|
+
# declarations alongside the regular walk so the
|
|
119
|
+
# plugin's chain lookup can lift sigs declared
|
|
120
|
+
# on a mixed-in module to the host class.
|
|
121
|
+
record_mixin_call(child, state, lexical_path) if mixin_call?(child) && !in_singleton_class
|
|
122
|
+
walk_node(child, state, lexical_path: lexical_path, in_singleton_class: in_singleton_class)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
state.record_error(:dangling_sig, pending_sig) if pending_sig
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `include Foo` / `extend Foo` / `include Foo, Bar` —
|
|
130
|
+
# the `include` and `extend` mixin macros that Tapioca-
|
|
131
|
+
# generated DSL RBIs depend on. Recognised when the
|
|
132
|
+
# call has no explicit receiver (top-level inside a
|
|
133
|
+
# class body) and every argument is a constant
|
|
134
|
+
# reference.
|
|
135
|
+
def mixin_call?(node)
|
|
136
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
137
|
+
return false unless %i[include extend].include?(node.name)
|
|
138
|
+
return false unless node.receiver.nil?
|
|
139
|
+
|
|
140
|
+
args = node.arguments&.arguments
|
|
141
|
+
return false if args.nil? || args.empty?
|
|
142
|
+
|
|
143
|
+
args.all? do |arg|
|
|
144
|
+
arg.is_a?(Prism::ConstantReadNode) || arg.is_a?(Prism::ConstantPathNode)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_mixin_call(node, state, lexical_path)
|
|
149
|
+
return if lexical_path.empty?
|
|
150
|
+
|
|
151
|
+
class_name = lexical_path.join("::")
|
|
152
|
+
kind = node.name == :include ? :include : :extend
|
|
153
|
+
(node.arguments&.arguments || []).each do |arg|
|
|
154
|
+
module_name = qualified_name_for(arg)
|
|
155
|
+
next if module_name.nil?
|
|
156
|
+
|
|
157
|
+
state.catalog.record_mixin(
|
|
158
|
+
class_name: class_name, kind: kind, module_name: module_name
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def sig_call?(node)
|
|
164
|
+
node.is_a?(Prism::CallNode) &&
|
|
165
|
+
node.name == :sig &&
|
|
166
|
+
node.receiver.nil? &&
|
|
167
|
+
!node.block.nil?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def def_node?(node)
|
|
171
|
+
node.is_a?(Prism::DefNode)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def record_def_with_sig(def_node, sig_call, state, lexical_path, in_singleton_class)
|
|
175
|
+
parsed = SigParser.parse(sig_call)
|
|
176
|
+
if parsed.is_a?(SigParser::ParseError)
|
|
177
|
+
state.record_error(parsed.reason, sig_call)
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class_name = lexical_path.empty? ? "Object" : lexical_path.join("::")
|
|
182
|
+
kind = singleton_method?(def_node, in_singleton_class) ? :singleton : :instance
|
|
183
|
+
catalog_record(state.catalog, class_name, def_node.name, kind, parsed)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def catalog_record(catalog, class_name, method_name, kind, parsed)
|
|
187
|
+
catalog.record(
|
|
188
|
+
MethodSignature.new(
|
|
189
|
+
class_name: class_name,
|
|
190
|
+
method_name: method_name,
|
|
191
|
+
kind: kind,
|
|
192
|
+
params: parsed.params,
|
|
193
|
+
return_type: parsed.return_type,
|
|
194
|
+
modifiers: parsed.modifiers
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def singleton_method?(def_node, in_singleton_class)
|
|
200
|
+
in_singleton_class || def_node.receiver.is_a?(Prism::SelfNode)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Resolves a constant-path node (`Foo::Bar`,
|
|
204
|
+
# `::Foo::Bar`) to its dot-separated name. Returns nil
|
|
205
|
+
# for the rare dynamic-prefix shape so the walker
|
|
206
|
+
# doesn't guess a qualified name in that case.
|
|
207
|
+
def qualified_name_for(node)
|
|
208
|
+
case node
|
|
209
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
210
|
+
when Prism::ConstantPathNode
|
|
211
|
+
parts = []
|
|
212
|
+
current = node
|
|
213
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
214
|
+
parts.unshift(current.name.to_s)
|
|
215
|
+
current = current.parent
|
|
216
|
+
end
|
|
217
|
+
case current
|
|
218
|
+
when nil then "::#{parts.join('::')}"
|
|
219
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|