rigortype 0.1.10 → 0.1.12
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/baseline.rb +51 -15
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli.rb +44 -3
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +181 -4
- 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 +199 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +398 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +86 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +183 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +713 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +201 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +226 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +261 -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 +283 -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 +250 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +98 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +590 -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 +37 -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 +478 -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 +353 -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 +175 -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 +350 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +164 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1538 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +235 -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/scope.rbs +22 -0
- metadata +157 -1
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
# Mini-interpreter for the chained-call expression that
|
|
11
|
+
# makes up a Sorbet `sig` block. The block body is always
|
|
12
|
+
# a single expression — Sorbet's docs (`sigs.md`) show the
|
|
13
|
+
# full grammar:
|
|
14
|
+
#
|
|
15
|
+
# sig { params(x: T, y: T).returns(U) }
|
|
16
|
+
# sig { void }
|
|
17
|
+
# sig { abstract.params(...).returns(...) }
|
|
18
|
+
# sig { override.params(...).void }
|
|
19
|
+
# sig { type_parameters(:U).params(...).returns(...) }
|
|
20
|
+
# sig do
|
|
21
|
+
# params(...)
|
|
22
|
+
# .returns(...)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# The parser walks the chain right-to-left, gathering
|
|
26
|
+
# whatever it recognises (`params` / `returns` / `void` /
|
|
27
|
+
# `abstract` / `override` / `overridable` / `final` /
|
|
28
|
+
# `type_parameters` / `checked` / `on_failure`) into a
|
|
29
|
+
# frozen result hash. Slice 1 wires the parsed structure
|
|
30
|
+
# into {MethodSignature}; later slices will start *acting*
|
|
31
|
+
# on the modifiers and `type_parameters`.
|
|
32
|
+
#
|
|
33
|
+
# The parser is intentionally tolerant — unknown chain
|
|
34
|
+
# nodes degrade to "the rest of the chain is opaque" rather
|
|
35
|
+
# than raising. The plugin emits a diagnostic
|
|
36
|
+
# (`plugin.sorbet.parse-error`) only when the entire chain
|
|
37
|
+
# fails to yield either a `returns` or a `void`.
|
|
38
|
+
module SigParser
|
|
39
|
+
# Modifiers we recognise at any position in the chain.
|
|
40
|
+
# Stored in `:modifiers` on the parse result.
|
|
41
|
+
RECOGNISED_MODIFIERS = %i[abstract override overridable final].freeze
|
|
42
|
+
|
|
43
|
+
# Sorbet runtime-only chain steps. Recognised so the
|
|
44
|
+
# parser doesn't degrade the whole sig when it sees them,
|
|
45
|
+
# but their payload is intentionally discarded.
|
|
46
|
+
RUNTIME_ONLY_STEPS = %i[checked on_failure].freeze
|
|
47
|
+
|
|
48
|
+
ParseResult = Data.define(:return_type, :params, :modifiers, :void) do
|
|
49
|
+
def void? = void
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ParseError = Data.define(:reason, :node)
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# @param sig_call [Prism::CallNode] the `sig { ... }` /
|
|
57
|
+
# `sig do ... end` call.
|
|
58
|
+
# @return [ParseResult, ParseError]
|
|
59
|
+
def parse(sig_call)
|
|
60
|
+
return ParseError.new(reason: :no_block, node: sig_call) if sig_call.block.nil?
|
|
61
|
+
|
|
62
|
+
body = sig_call.block.body
|
|
63
|
+
chain_root = first_statement(body)
|
|
64
|
+
return ParseError.new(reason: :empty_block, node: sig_call) if chain_root.nil?
|
|
65
|
+
|
|
66
|
+
fold_chain(chain_root, sig_call)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def first_statement(body)
|
|
70
|
+
case body
|
|
71
|
+
when Prism::StatementsNode then body.body.first
|
|
72
|
+
else body
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Walks the chain bottom-up. Each chain link is a
|
|
77
|
+
# `Prism::CallNode` whose receiver is the next link;
|
|
78
|
+
# `params` / `returns` / `void` may appear at any
|
|
79
|
+
# position, so we accumulate their effect into a
|
|
80
|
+
# mutable hash and freeze on the way out.
|
|
81
|
+
def fold_chain(node, sig_call)
|
|
82
|
+
accumulator = { return_type: nil, params: {}, modifiers: [], void: false, terminus_kind: nil }
|
|
83
|
+
current = node
|
|
84
|
+
|
|
85
|
+
while current.is_a?(Prism::CallNode)
|
|
86
|
+
case current.name
|
|
87
|
+
when :returns
|
|
88
|
+
accumulator[:return_type] = TypeTranslator.translate(first_argument(current))
|
|
89
|
+
accumulator[:terminus_kind] ||= :returns
|
|
90
|
+
when :void
|
|
91
|
+
accumulator[:void] = true
|
|
92
|
+
accumulator[:terminus_kind] ||= :void
|
|
93
|
+
when :params
|
|
94
|
+
accumulator[:params].merge!(parse_params(current))
|
|
95
|
+
when :type_parameters
|
|
96
|
+
# Slice 1: recognise to suppress the degraded
|
|
97
|
+
# path; widen translation in slice 3.
|
|
98
|
+
when *RECOGNISED_MODIFIERS
|
|
99
|
+
accumulator[:modifiers] << current.name
|
|
100
|
+
when *RUNTIME_ONLY_STEPS
|
|
101
|
+
# Discard payload; runtime-only.
|
|
102
|
+
else
|
|
103
|
+
# Unknown chain link — stop folding and treat
|
|
104
|
+
# whatever we accumulated so far as the result.
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
current = current.receiver
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return ParseError.new(reason: :missing_returns_or_void, node: sig_call) if accumulator[:terminus_kind].nil?
|
|
111
|
+
|
|
112
|
+
ParseResult.new(
|
|
113
|
+
return_type: resolve_return_type(accumulator),
|
|
114
|
+
params: accumulator[:params].freeze,
|
|
115
|
+
modifiers: accumulator[:modifiers].uniq.freeze,
|
|
116
|
+
void: accumulator[:void]
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# `void` and `returns(T)` share the slot; if both are
|
|
121
|
+
# present (unusual but parseable), `returns(T)` wins
|
|
122
|
+
# because Sorbet's static side treats `void` as
|
|
123
|
+
# "discard the value" — when the user explicitly named
|
|
124
|
+
# `T`, that's the more informative shape.
|
|
125
|
+
def resolve_return_type(accumulator)
|
|
126
|
+
accumulator[:return_type] || Rigor::Type::Combinator.untyped
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `params(x: Integer, y: T.nilable(String))` — extracts
|
|
130
|
+
# the `KeywordHashNode` AST and translates each value.
|
|
131
|
+
# The result is `{ Symbol => Rigor::Type }`. Splat /
|
|
132
|
+
# double-splat / unrecognised keys degrade silently
|
|
133
|
+
# (slice 1 behaviour).
|
|
134
|
+
def parse_params(call_node)
|
|
135
|
+
args = call_node.arguments&.arguments || []
|
|
136
|
+
first = args.first
|
|
137
|
+
return {} unless first.is_a?(Prism::KeywordHashNode)
|
|
138
|
+
|
|
139
|
+
first.elements.each_with_object({}) do |element, into|
|
|
140
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
141
|
+
next unless element.key.is_a?(Prism::SymbolNode)
|
|
142
|
+
|
|
143
|
+
key = element.key.unescaped.to_sym
|
|
144
|
+
into[key] = TypeTranslator.translate(element.value)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def first_argument(call_node)
|
|
149
|
+
call_node.arguments&.arguments&.first
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Sorbet < Rigor::Plugin::Base
|
|
6
|
+
# Reads Sorbet's `# typed: <level>` magic comment from the
|
|
7
|
+
# head of a file. Sorbet's own contract (per
|
|
8
|
+
# [`static.md`](https://sorbet.org/docs/static)) requires
|
|
9
|
+
# the sigil to appear at the top of the file before any
|
|
10
|
+
# Ruby code. We're slightly more lenient here — the sigil
|
|
11
|
+
# may appear after a few comment / blank lines (matching
|
|
12
|
+
# what Sorbet itself accepts in practice) but we stop
|
|
13
|
+
# scanning once we hit a non-comment, non-blank line.
|
|
14
|
+
#
|
|
15
|
+
# Recognised levels: `:ignore` / `:false` / `:true` /
|
|
16
|
+
# `:strict` / `:strong`. Falls back to `:false` (Sorbet's
|
|
17
|
+
# default) when no sigil is present, matching how Sorbet
|
|
18
|
+
# treats sigil-less files.
|
|
19
|
+
#
|
|
20
|
+
# Slice 5 of ADR-11 uses this purely at catalog-harvest
|
|
21
|
+
# time: `# typed: ignore` files are skipped entirely (the
|
|
22
|
+
# plugin records no sigs from them). The other levels are
|
|
23
|
+
# detected for forward compatibility but treated
|
|
24
|
+
# identically — per-call-site sigil honouring (e.g. only
|
|
25
|
+
# firing `T.let` recognition in `# typed: true`+ files)
|
|
26
|
+
# requires threading the file path through
|
|
27
|
+
# `flow_contribution_for`, which lives behind a future
|
|
28
|
+
# plugin-contract widening slice.
|
|
29
|
+
module SigilDetector
|
|
30
|
+
# Sorbet's strictness-level names. Stored as symbols to
|
|
31
|
+
# match the analyzer's existing convention for level
|
|
32
|
+
# identifiers; the `:true` / `:false` symbols here are
|
|
33
|
+
# level *names* (the textual sigil values) and are
|
|
34
|
+
# intentionally distinct from the `true` / `false`
|
|
35
|
+
# boolean literals.
|
|
36
|
+
VALID_LEVELS = %i[ignore false true strict strong].freeze
|
|
37
|
+
DEFAULT_LEVEL = :false # rubocop:disable Lint/BooleanSymbol
|
|
38
|
+
SIGIL_REGEX = /\A\s*#\s*typed\s*:\s*(ignore|false|true|strict|strong)\s*\z/
|
|
39
|
+
|
|
40
|
+
# Cap on how many lines we scan before giving up. Sorbet
|
|
41
|
+
# doesn't formally specify a cap, but the sigil
|
|
42
|
+
# convention is "near the top of the file"; 10 lines is
|
|
43
|
+
# generous and bounds the parse cost on enormous files.
|
|
44
|
+
MAX_HEAD_LINES = 10
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
# @param contents [String] raw file contents.
|
|
49
|
+
# @return [Symbol] one of {VALID_LEVELS}; defaults to
|
|
50
|
+
# {DEFAULT_LEVEL} for sigil-less or malformed-sigil
|
|
51
|
+
# files.
|
|
52
|
+
def detect(contents)
|
|
53
|
+
return DEFAULT_LEVEL if contents.nil? || contents.empty?
|
|
54
|
+
|
|
55
|
+
contents.each_line.with_index do |line, index|
|
|
56
|
+
break if index >= MAX_HEAD_LINES
|
|
57
|
+
|
|
58
|
+
stripped = line.strip
|
|
59
|
+
next if stripped.empty?
|
|
60
|
+
|
|
61
|
+
match = SIGIL_REGEX.match(stripped)
|
|
62
|
+
return match[1].to_sym if match
|
|
63
|
+
# First non-blank line that isn't a sigil-shaped
|
|
64
|
+
# comment ends the scan: Sorbet's parser stops at
|
|
65
|
+
# the first directive-or-code line.
|
|
66
|
+
break unless stripped.start_with?("#")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
DEFAULT_LEVEL
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param level [Symbol]
|
|
73
|
+
# @return [Boolean] true when `# typed: ignore`. The
|
|
74
|
+
# harvest pipeline calls this to short-circuit
|
|
75
|
+
# walking the file's AST.
|
|
76
|
+
def ignored?(level)
|
|
77
|
+
level == :ignore
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [Boolean] true when `level` is at or above the
|
|
81
|
+
# `# typed: true` mark. Used by the
|
|
82
|
+
# `enforce_sigil` config gate (default `true`): with
|
|
83
|
+
# the gate on, only files marked `:true` / `:strict` /
|
|
84
|
+
# `:strong` contribute their sigs to the catalog. The
|
|
85
|
+
# `:false` (and sigil-less) levels still get walked
|
|
86
|
+
# (so RBI files outside the project can be loaded
|
|
87
|
+
# regardless), but their sig-derived narrowing is
|
|
88
|
+
# suppressed — matching how Sorbet itself only
|
|
89
|
+
# enforces type errors at `# typed: true`+. Assertion
|
|
90
|
+
# recognisers (`T.let` / `T.cast` / `T.must` /
|
|
91
|
+
# `T.bind` / `T.assert_type!`) are NOT gated by this:
|
|
92
|
+
# the user wrote them deliberately, so the
|
|
93
|
+
# recogniser still fires regardless of sigil.
|
|
94
|
+
def enforced?(level)
|
|
95
|
+
%i[true strict strong].include?(level)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Sorbet < Rigor::Plugin::Base
|
|
8
|
+
# Maps Sorbet's type expressions (the AST inside a `sig`
|
|
9
|
+
# block's `params(...)` and `returns(...)` clauses) into
|
|
10
|
+
# Rigor's internal type carriers.
|
|
11
|
+
#
|
|
12
|
+
# Slice 1 covered the minimum vocabulary that lets a
|
|
13
|
+
# typical `sig { params(x: Integer).returns(String) }`
|
|
14
|
+
# round-trip; slice 3 widens it to cover the dense middle
|
|
15
|
+
# of Sorbet's surface — generic class applications
|
|
16
|
+
# (`T::Array[E]`, `T::Hash[K, V]`, etc.), class-object
|
|
17
|
+
# types (`T.class_of(C)`, `T::Class[T]`), tuples, and
|
|
18
|
+
# shapes:
|
|
19
|
+
#
|
|
20
|
+
# | Sorbet form | Rigor carrier |
|
|
21
|
+
# | ------------------------ | ---------------------------------------- |
|
|
22
|
+
# | `Integer` etc. | `Nominal["Integer"]` |
|
|
23
|
+
# | `::Foo::Bar` | `Nominal["Foo::Bar"]` |
|
|
24
|
+
# | `T.untyped` | `Dynamic[top]` |
|
|
25
|
+
# | `T.anything` | `top` |
|
|
26
|
+
# | `T.noreturn` | `bot` |
|
|
27
|
+
# | `T.nilable(X)` | `Union[X, Constant[nil]]` |
|
|
28
|
+
# | `T.any(A, B, ...)` | `Union[A, B, ...]` |
|
|
29
|
+
# | `T.all(A, B, ...)` | `Intersection[A, B, ...]` |
|
|
30
|
+
# | `T::Boolean` | `Union[Constant[true], Constant[false]]` |
|
|
31
|
+
# | `T::Array[E]` | `Nominal["Array", [E]]` |
|
|
32
|
+
# | `T::Hash[K, V]` | `Nominal["Hash", [K, V]]` |
|
|
33
|
+
# | `T::Set[E]` | `Nominal["Set", [E]]` |
|
|
34
|
+
# | `T::Range[E]` | `Nominal["Range", [E]]` |
|
|
35
|
+
# | `T::Enumerable[E]` | `Nominal["Enumerable", [E]]` |
|
|
36
|
+
# | `T::Enumerator[E]` | `Nominal["Enumerator", [E]]` |
|
|
37
|
+
# | `T::Class[T]` | `Singleton[T-class-name]` (lossy) |
|
|
38
|
+
# | `T.class_of(C)` | `Singleton[C]` |
|
|
39
|
+
# | `[A, B]` (tuple in sig) | `Tuple[A, B]` |
|
|
40
|
+
# | `{a: A, b: B}` (shape) | `HashShape{a: A, b: B}` (closed) |
|
|
41
|
+
#
|
|
42
|
+
# Anything else (`T.proc`, `T.attached_class`,
|
|
43
|
+
# `T.self_type`, `T.type_parameter`, `T::Struct` / `T::Enum`
|
|
44
|
+
# subclasses, …) degrades to `Dynamic[top]`. The degraded
|
|
45
|
+
# path stays silent for now per ADR-11's slice plan; a
|
|
46
|
+
# later slice surfaces the gap as a `dynamic.sorbet.unsupported`
|
|
47
|
+
# diagnostic.
|
|
48
|
+
module TypeTranslator
|
|
49
|
+
BOOLEAN_NAME = "Boolean"
|
|
50
|
+
|
|
51
|
+
# `T::*` constants whose `[]` application maps directly
|
|
52
|
+
# onto a Rigor `Nominal` with the matching standard-
|
|
53
|
+
# library class name. Ordering matches the table above
|
|
54
|
+
# for ease of reading.
|
|
55
|
+
T_GENERIC_CLASSES = {
|
|
56
|
+
"Array" => "Array",
|
|
57
|
+
"Hash" => "Hash",
|
|
58
|
+
"Set" => "Set",
|
|
59
|
+
"Range" => "Range",
|
|
60
|
+
"Enumerable" => "Enumerable",
|
|
61
|
+
"Enumerator" => "Enumerator",
|
|
62
|
+
"Enumerator::Lazy" => "Enumerator::Lazy",
|
|
63
|
+
"Enumerator::Chain" => "Enumerator::Chain"
|
|
64
|
+
}.freeze
|
|
65
|
+
|
|
66
|
+
module_function
|
|
67
|
+
|
|
68
|
+
# @param node [Prism::Node, nil]
|
|
69
|
+
# @return [Rigor::Type] never `nil`; unrecognised forms
|
|
70
|
+
# degrade to `Type::Combinator.untyped`.
|
|
71
|
+
def translate(node)
|
|
72
|
+
return Rigor::Type::Combinator.untyped if node.nil?
|
|
73
|
+
|
|
74
|
+
case node
|
|
75
|
+
when Prism::ConstantReadNode then translate_constant_read(node)
|
|
76
|
+
when Prism::ConstantPathNode then translate_constant_path(node)
|
|
77
|
+
when Prism::CallNode then translate_call(node)
|
|
78
|
+
when Prism::ArrayNode then translate_tuple(node)
|
|
79
|
+
when Prism::HashNode then translate_shape(node)
|
|
80
|
+
else degraded
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param node [Prism::ConstantReadNode]
|
|
85
|
+
def translate_constant_read(node)
|
|
86
|
+
name = node.name.to_s
|
|
87
|
+
return Rigor::Type::Combinator.untyped if name.empty?
|
|
88
|
+
|
|
89
|
+
Rigor::Type::Combinator.nominal_of(name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @param node [Prism::ConstantPathNode]
|
|
93
|
+
def translate_constant_path(node)
|
|
94
|
+
name = constant_path_name(node)
|
|
95
|
+
return degraded if name.nil?
|
|
96
|
+
|
|
97
|
+
# Sorbet's `T::Boolean` is a special alias rather than a
|
|
98
|
+
# nominal class, expressed as the Boolean type alias.
|
|
99
|
+
return boolean_type if name == "T::Boolean"
|
|
100
|
+
|
|
101
|
+
Rigor::Type::Combinator.nominal_of(name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# `Prism::CallNode` covers two distinct surfaces:
|
|
105
|
+
#
|
|
106
|
+
# 1. `T.something(...)` — `untyped` / `anything` /
|
|
107
|
+
# `noreturn` / `nilable` / `any` / `all` / `class_of`.
|
|
108
|
+
# 2. `T::SomeClass[...]` — the `[]` method on a generic
|
|
109
|
+
# `T::*` constant (slice 3 widening). Maps to
|
|
110
|
+
# `Nominal[name, type_args]`.
|
|
111
|
+
def translate_call(node)
|
|
112
|
+
return translate_t_method(node) if sorbet_t_namespaced?(node.receiver)
|
|
113
|
+
return translate_t_subscript(node) if sorbet_subscript?(node)
|
|
114
|
+
|
|
115
|
+
degraded
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Handles the `T.foo(...)` family.
|
|
119
|
+
def translate_t_method(node)
|
|
120
|
+
case node.name
|
|
121
|
+
when :untyped then Rigor::Type::Combinator.untyped
|
|
122
|
+
when :anything then Rigor::Type::Combinator.top
|
|
123
|
+
when :noreturn then Rigor::Type::Combinator.bot
|
|
124
|
+
when :nilable then translate_nilable(node)
|
|
125
|
+
when :any then translate_any(node)
|
|
126
|
+
when :all then translate_all(node)
|
|
127
|
+
when :class_of then translate_class_of(node)
|
|
128
|
+
else degraded
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def translate_nilable(node)
|
|
133
|
+
inner = first_argument(node)
|
|
134
|
+
return degraded if inner.nil?
|
|
135
|
+
|
|
136
|
+
Rigor::Type::Combinator.union(
|
|
137
|
+
translate(inner), Rigor::Type::Combinator.constant_of(nil)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def translate_any(node)
|
|
142
|
+
args = call_arguments(node)
|
|
143
|
+
return degraded if args.empty?
|
|
144
|
+
|
|
145
|
+
Rigor::Type::Combinator.union(*args.map { |arg| translate(arg) })
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def translate_all(node)
|
|
149
|
+
args = call_arguments(node)
|
|
150
|
+
return degraded if args.empty?
|
|
151
|
+
|
|
152
|
+
Rigor::Type::Combinator.intersection(*args.map { |arg| translate(arg) })
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# `T.class_of(C)` — singleton-class type for a single
|
|
156
|
+
# constant. Sorbet docs note `T.class_of(MyInterface)`
|
|
157
|
+
# rarely means what users expect (it's the singleton
|
|
158
|
+
# class of `MyInterface`, not "any class implementing
|
|
159
|
+
# the interface"); we honour the literal meaning here
|
|
160
|
+
# and translate to `Singleton[C]`.
|
|
161
|
+
def translate_class_of(node)
|
|
162
|
+
target = first_argument(node)
|
|
163
|
+
name = constant_path_name(target)
|
|
164
|
+
return degraded if name.nil?
|
|
165
|
+
|
|
166
|
+
Rigor::Type::Combinator.singleton_of(name)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Handles `T::Array[E]`, `T::Hash[K, V]`, etc. The Prism
|
|
170
|
+
# AST for `T::Array[Integer]` is a `CallNode` whose
|
|
171
|
+
# receiver is the `T::Array` `ConstantPathNode` and
|
|
172
|
+
# whose `name` is `:[]`. `T::Class[T]` lands here too;
|
|
173
|
+
# we collapse it to `Singleton[name]` (a deliberate
|
|
174
|
+
# narrowing — `T::Class` is structurally generic in
|
|
175
|
+
# Sorbet, but Rigor's `Singleton` carries class identity
|
|
176
|
+
# only).
|
|
177
|
+
def translate_t_subscript(node)
|
|
178
|
+
base_name = sorbet_subscript_base(node.receiver)
|
|
179
|
+
args = call_arguments(node).map { |arg| translate(arg) }
|
|
180
|
+
mapped = T_GENERIC_CLASSES[base_name]
|
|
181
|
+
|
|
182
|
+
if mapped
|
|
183
|
+
Rigor::Type::Combinator.nominal_of(mapped, type_args: args)
|
|
184
|
+
elsif base_name == "Class"
|
|
185
|
+
translate_t_class_subscript(args)
|
|
186
|
+
else
|
|
187
|
+
degraded
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `T::Class[T]` — Sorbet's "any class object whose
|
|
192
|
+
# instances are at least `T`". Rigor has no exact
|
|
193
|
+
# analogue (Singleton names a specific class); the
|
|
194
|
+
# closest faithful translation is `Singleton[name]`
|
|
195
|
+
# when `T` is a constant, or `Singleton[Object]` for
|
|
196
|
+
# broader applications. Lossy translation; emitted as
|
|
197
|
+
# `dynamic.sorbet.degraded` once slice 3's diagnostic
|
|
198
|
+
# surface lands.
|
|
199
|
+
def translate_t_class_subscript(args)
|
|
200
|
+
inner = args.first
|
|
201
|
+
return Rigor::Type::Combinator.singleton_of("Class") if inner.nil?
|
|
202
|
+
|
|
203
|
+
case inner
|
|
204
|
+
when Rigor::Type::Nominal then Rigor::Type::Combinator.singleton_of(inner.class_name)
|
|
205
|
+
else Rigor::Type::Combinator.singleton_of("Class")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Tuple types in `sig` position appear as bare array
|
|
210
|
+
# literals: `sig { returns([String, Integer]) }`. Each
|
|
211
|
+
# element is itself a type expression we translate
|
|
212
|
+
# recursively.
|
|
213
|
+
def translate_tuple(node)
|
|
214
|
+
elements = node.elements.map { |element| translate(element) }
|
|
215
|
+
Rigor::Type::Combinator.tuple_of(*elements)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Shape types in `sig` position appear as bare hash
|
|
219
|
+
# literals with symbol keys:
|
|
220
|
+
# `sig { returns({a: Integer, b: String}) }`. Each
|
|
221
|
+
# value is a type expression; the resulting `HashShape`
|
|
222
|
+
# is closed (no extra keys allowed).
|
|
223
|
+
def translate_shape(node)
|
|
224
|
+
pairs = []
|
|
225
|
+
node.elements.each do |element|
|
|
226
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
227
|
+
next unless element.key.is_a?(Prism::SymbolNode)
|
|
228
|
+
|
|
229
|
+
pairs << [element.key.unescaped.to_sym, translate(element.value)]
|
|
230
|
+
end
|
|
231
|
+
Rigor::Type::Combinator.hash_shape_of(pairs)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Renders a constant-path node (`Foo::Bar`, `::Foo::Bar`)
|
|
235
|
+
# as a `::`-joined String. Mirrors the helper used by
|
|
236
|
+
# rigor-activerecord's ModelDiscoverer for parity.
|
|
237
|
+
def constant_path_name(node)
|
|
238
|
+
return nil if node.nil?
|
|
239
|
+
|
|
240
|
+
case node
|
|
241
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
242
|
+
when Prism::ConstantPathNode then constant_path_name_for_path(node)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def constant_path_name_for_path(node)
|
|
247
|
+
parts = []
|
|
248
|
+
current = node
|
|
249
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
250
|
+
parts.unshift(current.name.to_s)
|
|
251
|
+
current = current.parent
|
|
252
|
+
end
|
|
253
|
+
case current
|
|
254
|
+
when nil
|
|
255
|
+
"::#{parts.join('::')}"
|
|
256
|
+
when Prism::ConstantReadNode
|
|
257
|
+
"#{current.name}::#{parts.join('::')}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def sorbet_t_namespaced?(receiver)
|
|
262
|
+
receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :T
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# `T::Array[Integer]` parses as `CallNode(receiver: T::Array, name: :[])`.
|
|
266
|
+
# The receiver is a `ConstantPathNode` rooted at the
|
|
267
|
+
# `T` constant.
|
|
268
|
+
def sorbet_subscript?(node)
|
|
269
|
+
node.name == :[] && sorbet_t_qualified?(node.receiver)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def sorbet_t_qualified?(node)
|
|
273
|
+
return false unless node.is_a?(Prism::ConstantPathNode)
|
|
274
|
+
|
|
275
|
+
# Walk to the root; require that it terminates at a
|
|
276
|
+
# `T` ConstantReadNode (not an absolute `::T`).
|
|
277
|
+
current = node
|
|
278
|
+
current = current.parent while current.is_a?(Prism::ConstantPathNode)
|
|
279
|
+
current.is_a?(Prism::ConstantReadNode) && current.name == :T
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Strips the leading `T::` from a `T::Foo::Bar`
|
|
283
|
+
# constant-path node, returning `"Foo::Bar"`. Returns
|
|
284
|
+
# nil for shapes that aren't `T`-rooted.
|
|
285
|
+
def sorbet_subscript_base(node)
|
|
286
|
+
return nil unless sorbet_t_qualified?(node)
|
|
287
|
+
|
|
288
|
+
parts = []
|
|
289
|
+
current = node
|
|
290
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
291
|
+
parts.unshift(current.name.to_s)
|
|
292
|
+
current = current.parent
|
|
293
|
+
end
|
|
294
|
+
parts.join("::")
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def first_argument(node)
|
|
298
|
+
node.arguments&.arguments&.first
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def call_arguments(node)
|
|
302
|
+
node.arguments&.arguments || []
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def degraded
|
|
306
|
+
Rigor::Type::Combinator.untyped
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# `T::Boolean` corresponds to the union of the singleton
|
|
310
|
+
# `true` / `false` values, matching how RBS's `bool`
|
|
311
|
+
# would translate. Built from `Constant[true]` /
|
|
312
|
+
# `Constant[false]` so the analyzer's flow-sensitive
|
|
313
|
+
# narrowing recognises the discriminating shape.
|
|
314
|
+
def boolean_type
|
|
315
|
+
Rigor::Type::Combinator.union(
|
|
316
|
+
Rigor::Type::Combinator.constant_of(true),
|
|
317
|
+
Rigor::Type::Combinator.constant_of(false)
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|