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,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
# ADR-32 — bundled `rigor-rbs-inline` plugin.
|
|
6
|
+
#
|
|
7
|
+
# Synthesises RBS from project Ruby files that carry
|
|
8
|
+
# rbs-inline-shaped comments (`#: () -> T`, `# @rbs name: T`,
|
|
9
|
+
# `# @rbs return: T`, attribute `#:`, …) and contributes the
|
|
10
|
+
# result to the analysis environment through the
|
|
11
|
+
# `source_rbs_synthesizer:` manifest hook.
|
|
12
|
+
#
|
|
13
|
+
# By default (WD2) only files starting with the upstream
|
|
14
|
+
# `# rbs_inline: enabled` magic comment are processed; a host
|
|
15
|
+
# context can flip this off by setting `require_magic_comment:
|
|
16
|
+
# false` in the plugin config (WD10).
|
|
17
|
+
module Rigor
|
|
18
|
+
module Plugin
|
|
19
|
+
# The plugin gem requires `rbs/inline` at load time; without
|
|
20
|
+
# the upstream library the synthesizer can't do its job.
|
|
21
|
+
# Wrapped in a begin/rescue so the analyzer still loads if
|
|
22
|
+
# the user activated this plugin without installing the
|
|
23
|
+
# `rbs-inline` gem (loud-on-activation, fail-soft to no
|
|
24
|
+
# contribution).
|
|
25
|
+
begin
|
|
26
|
+
require "prism"
|
|
27
|
+
require "rbs/inline"
|
|
28
|
+
RBS_INLINE_AVAILABLE = true
|
|
29
|
+
rescue ::LoadError => e
|
|
30
|
+
warn(
|
|
31
|
+
"rigor-rbs-inline: failed to load `rbs/inline` " \
|
|
32
|
+
"(#{e.message}). The plugin will load but contribute no " \
|
|
33
|
+
"synthesised RBS. Install the `rbs-inline` gem to enable " \
|
|
34
|
+
"inline-RBS comment ingestion."
|
|
35
|
+
)
|
|
36
|
+
RBS_INLINE_AVAILABLE = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class RbsInline < Rigor::Plugin::Base
|
|
40
|
+
# Synthesizer callable invoked once per project Ruby
|
|
41
|
+
# source file by `Environment.for_project` at env-build
|
|
42
|
+
# time. Returns the synthesised RBS source as a String,
|
|
43
|
+
# or `nil` when the file contributes nothing (no magic
|
|
44
|
+
# comment in the default mode, empty annotation set,
|
|
45
|
+
# parse error per WD6).
|
|
46
|
+
class Synthesizer
|
|
47
|
+
# @param require_magic_comment [Boolean] when `true`
|
|
48
|
+
# (the default, WD2), only files with
|
|
49
|
+
# `# rbs_inline: enabled` at the top are processed.
|
|
50
|
+
# When `false` (WD10 host-context override), every
|
|
51
|
+
# file is treated as if it carried the magic comment.
|
|
52
|
+
def initialize(require_magic_comment:)
|
|
53
|
+
@require_magic_comment = require_magic_comment
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return value contract:
|
|
58
|
+
# - `String` (non-empty) → successful synthesis
|
|
59
|
+
# - `nil` → no contribution
|
|
60
|
+
# - `[:error, message_string]` → parse failed, surface
|
|
61
|
+
# info diagnostic per ADR-32 WD6
|
|
62
|
+
def call(source_file_path)
|
|
63
|
+
return nil unless RBS_INLINE_AVAILABLE
|
|
64
|
+
return nil unless File.file?(source_file_path)
|
|
65
|
+
|
|
66
|
+
source = File.read(source_file_path)
|
|
67
|
+
return nil if source.empty?
|
|
68
|
+
|
|
69
|
+
result = ::Prism.parse(source)
|
|
70
|
+
# `opt_in: true` is rbs-inline's "require the magic
|
|
71
|
+
# comment" mode (per upstream parser.rb:62). The
|
|
72
|
+
# plugin's `require_magic_comment:` config knob maps
|
|
73
|
+
# directly onto it.
|
|
74
|
+
parsed = ::RBS::Inline::Parser.parse(result, opt_in: @require_magic_comment)
|
|
75
|
+
return nil if parsed.nil?
|
|
76
|
+
|
|
77
|
+
uses, decls, rbs_decls = parsed
|
|
78
|
+
rendered = ::RBS::Inline::Writer.write(uses, decls, rbs_decls)
|
|
79
|
+
return nil if rendered.nil? || rendered.strip.empty?
|
|
80
|
+
|
|
81
|
+
rendered
|
|
82
|
+
rescue ::StandardError => e
|
|
83
|
+
# WD6 fail-soft — surface a structured error tuple so
|
|
84
|
+
# the engine's `Environment.for_project` can emit a
|
|
85
|
+
# `source-rbs-synthesis-failed` info diagnostic
|
|
86
|
+
# naming the file + the upstream error message,
|
|
87
|
+
# without crashing analysis.
|
|
88
|
+
[:error, "#{e.class}: #{e.message.to_s.lines.first.to_s.strip}"]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
manifest(
|
|
93
|
+
id: "rbs-inline",
|
|
94
|
+
version: "0.1.0",
|
|
95
|
+
description: "Ingests rbs-inline-shaped comments " \
|
|
96
|
+
"(`# @rbs name: T`, `#: () -> T`, …) as RBS contributions.",
|
|
97
|
+
config_schema: { "require_magic_comment" => :boolean },
|
|
98
|
+
source_rbs_synthesizer: nil # set per-instance below
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Per-instance synthesizer — built from the manifest's
|
|
102
|
+
# default + the project's plugin config. The manifest
|
|
103
|
+
# `source_rbs_synthesizer:` is nil at the class level so
|
|
104
|
+
# the registry sees the instance's override (returned by
|
|
105
|
+
# `#manifest`, which `Plugin::Registry#source_rbs_synthesizers`
|
|
106
|
+
# consults via `plugin.manifest.source_rbs_synthesizer`).
|
|
107
|
+
#
|
|
108
|
+
# ADR-32 WD10 — `require_magic_comment` defaults to
|
|
109
|
+
# `true`. Setting it to `false` in `.rigor.yml` flips the
|
|
110
|
+
# synthesizer into "process every file" mode.
|
|
111
|
+
def initialize(services:, config: {})
|
|
112
|
+
super
|
|
113
|
+
@require_magic_comment = config.fetch("require_magic_comment", true) ? true : false
|
|
114
|
+
@synthesizer = Synthesizer.new(require_magic_comment: @require_magic_comment)
|
|
115
|
+
# Build the per-instance manifest eagerly (before
|
|
116
|
+
# `freeze`) so the registry's repeated reads return
|
|
117
|
+
# the same object and we don't need to mutate a
|
|
118
|
+
# frozen instance later.
|
|
119
|
+
base = self.class.manifest
|
|
120
|
+
@manifest_with_synth = build_manifest_with_synthesizer(base)
|
|
121
|
+
freeze
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
attr_reader :synthesizer
|
|
125
|
+
|
|
126
|
+
# Override the manifest-level `source_rbs_synthesizer:`
|
|
127
|
+
# (which is nil at the class level) with the per-instance
|
|
128
|
+
# synthesizer built from the merged config. The registry
|
|
129
|
+
# reads this through `plugin.manifest.source_rbs_synthesizer`.
|
|
130
|
+
def manifest
|
|
131
|
+
@manifest_with_synth
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def build_manifest_with_synthesizer(base)
|
|
137
|
+
Rigor::Plugin::Manifest.new(
|
|
138
|
+
id: base.id,
|
|
139
|
+
version: base.version,
|
|
140
|
+
description: base.description,
|
|
141
|
+
protocols: base.protocols,
|
|
142
|
+
config_schema: base.config_schema,
|
|
143
|
+
produces: base.produces,
|
|
144
|
+
consumes: base.consumes,
|
|
145
|
+
owns_receivers: base.owns_receivers,
|
|
146
|
+
open_receivers: base.open_receivers,
|
|
147
|
+
type_node_resolvers: base.type_node_resolvers,
|
|
148
|
+
block_as_methods: base.block_as_methods,
|
|
149
|
+
heredoc_templates: base.heredoc_templates,
|
|
150
|
+
trait_registries: base.trait_registries,
|
|
151
|
+
external_files: base.external_files,
|
|
152
|
+
hkt_registrations: base.hkt_registrations,
|
|
153
|
+
hkt_definitions: base.hkt_definitions,
|
|
154
|
+
signature_paths: base.signature_paths,
|
|
155
|
+
protocol_contracts: base.protocol_contracts,
|
|
156
|
+
source_rbs_synthesizer: @synthesizer
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
Rigor::Plugin.register(RbsInline)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rigor-rbs-inline — ingests rbs-inline-shaped comments
|
|
4
|
+
# (`# @rbs name: T`, `#: () -> T`, `# @rbs return: T`, attribute
|
|
5
|
+
# `#:`, ivars, generics, override, …) as RBS contributions to
|
|
6
|
+
# the analysis environment.
|
|
7
|
+
#
|
|
8
|
+
# Per ADR-32 the plugin gates per file on the upstream
|
|
9
|
+
# `# rbs_inline: enabled` magic comment by default. A host
|
|
10
|
+
# context that owns the entire analysis scope (e.g. the
|
|
11
|
+
# ADR-29 browser playground) can set `require_magic_comment:
|
|
12
|
+
# false` in the plugin config to skip the gate — every file
|
|
13
|
+
# the synthesizer sees is treated as if it carried the magic
|
|
14
|
+
# comment.
|
|
15
|
+
#
|
|
16
|
+
# # .rigor.yml
|
|
17
|
+
# plugins:
|
|
18
|
+
# - id: rigor-rbs-inline
|
|
19
|
+
# config:
|
|
20
|
+
# require_magic_comment: false # default true
|
|
21
|
+
#
|
|
22
|
+
# The plugin depends on the upstream `rbs-inline` gem; Rigor's
|
|
23
|
+
# core `rigortype` gemspec stays zero-dep per ADR-0 / ADR-32 WD1.
|
|
24
|
+
require_relative "rigor/plugin/rbs_inline"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "scope_walker"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Rspec < Rigor::Plugin::Base
|
|
10
|
+
# Per-file walker that:
|
|
11
|
+
#
|
|
12
|
+
# 1. Collects every RSpec scope (each `RSpec.describe`
|
|
13
|
+
# plus its nested `describe` / `context` blocks)
|
|
14
|
+
# via {ScopeWalker}.
|
|
15
|
+
# 2. Reports duplicate `let(:name)` / `subject(:name)`
|
|
16
|
+
# declarations within the same scope (the second
|
|
17
|
+
# declaration wins at runtime — an easy
|
|
18
|
+
# copy-paste bug).
|
|
19
|
+
# 3. Reports recursive self-references —
|
|
20
|
+
# `let(:user) { user.something }` will infinite-loop
|
|
21
|
+
# at runtime — an easy oversight.
|
|
22
|
+
module Analyzer
|
|
23
|
+
Diagnostic = Struct.new(:path, :line, :column, :severity, :rule, :message, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param path [String]
|
|
28
|
+
# @param root [Prism::Node]
|
|
29
|
+
# @return [Array<Diagnostic>]
|
|
30
|
+
def diagnose(path:, root:)
|
|
31
|
+
diagnostics = []
|
|
32
|
+
ScopeWalker.collect_scopes(root).each do |outer|
|
|
33
|
+
ScopeWalker.each_scope(outer) do |scope|
|
|
34
|
+
diagnostics.concat(duplicate_diagnostics(path, scope))
|
|
35
|
+
diagnostics.concat(self_reference_diagnostics(path, scope))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
diagnostics
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def duplicate_diagnostics(path, scope)
|
|
42
|
+
counts = Hash.new { |h, k| h[k] = [] }
|
|
43
|
+
scope.declarations.each { |decl| counts[decl.name] << decl }
|
|
44
|
+
counts.flat_map do |name, decls|
|
|
45
|
+
next [] if decls.size < 2
|
|
46
|
+
|
|
47
|
+
duplicate_diagnostics_for(path, name, decls)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def duplicate_diagnostics_for(path, name, decls)
|
|
52
|
+
# Report each subsequent occurrence; the first
|
|
53
|
+
# one is the "winner" only by literal source
|
|
54
|
+
# order, but RSpec lets the LAST declaration win
|
|
55
|
+
# at runtime, so flag everything after the first
|
|
56
|
+
# so the user can see the full list.
|
|
57
|
+
decls.drop(1).map do |decl|
|
|
58
|
+
Diagnostic.new(
|
|
59
|
+
path: path,
|
|
60
|
+
line: decl.location.start_line,
|
|
61
|
+
column: decl.location.start_column + 1,
|
|
62
|
+
severity: :warning,
|
|
63
|
+
rule: "duplicate-let",
|
|
64
|
+
message: "duplicate `#{decl.kind}(:#{name})` in this scope " \
|
|
65
|
+
"(first declared at line #{decls.first.location.start_line}); " \
|
|
66
|
+
"the last declaration wins at runtime"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self_reference_diagnostics(path, scope)
|
|
72
|
+
scope.declarations.flat_map do |decl|
|
|
73
|
+
next [] unless self_references?(decl)
|
|
74
|
+
|
|
75
|
+
[self_reference_diagnostic(path, decl)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Walks the declaration's block body looking for a
|
|
80
|
+
# call to its own name with no explicit receiver.
|
|
81
|
+
# Returns true if at least one such call exists.
|
|
82
|
+
def self_references?(decl)
|
|
83
|
+
body = decl.block_node&.body
|
|
84
|
+
return false if body.nil?
|
|
85
|
+
|
|
86
|
+
contains_self_reference?(body, decl.name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def contains_self_reference?(node, name)
|
|
90
|
+
return false unless node.is_a?(Prism::Node)
|
|
91
|
+
return true if node.is_a?(Prism::CallNode) && node.name == name && node.receiver.nil?
|
|
92
|
+
|
|
93
|
+
node.compact_child_nodes.any? { |child| contains_self_reference?(child, name) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self_reference_diagnostic(path, decl)
|
|
97
|
+
Diagnostic.new(
|
|
98
|
+
path: path,
|
|
99
|
+
line: decl.location.start_line,
|
|
100
|
+
column: decl.location.start_column + 1,
|
|
101
|
+
severity: :error,
|
|
102
|
+
rule: "self-reference",
|
|
103
|
+
message: "`#{decl.kind}(:#{decl.name})` references its own name `#{decl.name}` — " \
|
|
104
|
+
"this will infinite-loop at runtime"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "scope_walker"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class Rspec < Rigor::Plugin::Base
|
|
10
|
+
# Per-file index of RSpec scope hierarchy. Each
|
|
11
|
+
# describe scope carries:
|
|
12
|
+
#
|
|
13
|
+
# - `outer_range`: `Range[Integer]` of line numbers
|
|
14
|
+
# covered by the describe's block body (start_line..end_line).
|
|
15
|
+
# - `describe_const`: the model constant string named in
|
|
16
|
+
# `RSpec.describe SomeModel do` / `describe SomeModel do`,
|
|
17
|
+
# or `nil` when the describe's first arg is a String
|
|
18
|
+
# (`describe ".foo"`).
|
|
19
|
+
# - `lets`: `Hash{Symbol => Prism::BlockNode}` of `let(:name)
|
|
20
|
+
# { ... }` and `subject(:name) { ... }` declarations.
|
|
21
|
+
# `:subject` is the key for the implicit `subject { ... }`.
|
|
22
|
+
#
|
|
23
|
+
# Pillar 2 Slice 2 — used by the plugin's
|
|
24
|
+
# `flow_contribution_for` hook to bind `let`-named
|
|
25
|
+
# method-shape calls inside `it` bodies to the let
|
|
26
|
+
# block's inferred type.
|
|
27
|
+
class LetScopeIndex
|
|
28
|
+
Record = Struct.new(:outer_range, :describe_const, :lets, keyword_init: true) do
|
|
29
|
+
def contains?(line) = outer_range.cover?(line)
|
|
30
|
+
def let?(name) = lets.key?(name.to_sym)
|
|
31
|
+
def let_block(name) = lets[name.to_sym]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :records
|
|
35
|
+
|
|
36
|
+
def initialize(records)
|
|
37
|
+
@records = records.freeze
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns every record whose outer_range contains the
|
|
42
|
+
# given line, ordered from outermost to innermost. The
|
|
43
|
+
# innermost wins on `let` name collisions per Ruby's
|
|
44
|
+
# method-resolution order (RSpec nested `let` shadows
|
|
45
|
+
# the outer `let`).
|
|
46
|
+
def records_at(line)
|
|
47
|
+
@records.select { |rec| rec.contains?(line) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves a `let` name at the given line by walking
|
|
51
|
+
# records innermost to outermost.
|
|
52
|
+
# @return [Prism::BlockNode, nil]
|
|
53
|
+
def let_block_at(line, name)
|
|
54
|
+
name_sym = name.to_sym
|
|
55
|
+
records_at(line).reverse.each do |rec|
|
|
56
|
+
return rec.let_block(name_sym) if rec.let?(name_sym)
|
|
57
|
+
end
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Resolves the `describe`-anchor constant name at the
|
|
62
|
+
# given line. The innermost describe with a constant
|
|
63
|
+
# anchor wins; describe-with-String anchors are skipped.
|
|
64
|
+
# @return [String, nil]
|
|
65
|
+
def describe_const_at(line)
|
|
66
|
+
records_at(line).reverse.each do |rec|
|
|
67
|
+
return rec.describe_const if rec.describe_const
|
|
68
|
+
end
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Builds an index from a parsed root node.
|
|
73
|
+
def self.build(root)
|
|
74
|
+
records = []
|
|
75
|
+
collect(root, anchor: nil, accumulator: records)
|
|
76
|
+
new(records)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.collect(node, anchor:, accumulator:)
|
|
80
|
+
return unless node.is_a?(Prism::Node)
|
|
81
|
+
|
|
82
|
+
if describe_call?(node)
|
|
83
|
+
record = build_record(node, anchor)
|
|
84
|
+
accumulator << record
|
|
85
|
+
if node.block&.body
|
|
86
|
+
collect(node.block.body, anchor: record.describe_const || anchor, accumulator: accumulator)
|
|
87
|
+
end
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
node.compact_child_nodes.each do |child|
|
|
92
|
+
collect(child, anchor: anchor, accumulator: accumulator)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.build_record(describe_call, parent_anchor)
|
|
97
|
+
block_body = describe_call.block.body
|
|
98
|
+
start_line = block_body&.location&.start_line || describe_call.location.start_line
|
|
99
|
+
end_line = block_body&.location&.end_line || describe_call.block.location.end_line
|
|
100
|
+
|
|
101
|
+
Record.new(
|
|
102
|
+
outer_range: (start_line..end_line),
|
|
103
|
+
describe_const: describe_call_const(describe_call) || parent_anchor,
|
|
104
|
+
lets: collect_lets(describe_call.block.body)
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.describe_call?(node)
|
|
109
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
110
|
+
return false unless ScopeWalker::SCOPE_METHODS.include?(node.name) || node.name == :describe
|
|
111
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
112
|
+
|
|
113
|
+
# `RSpec.describe ...` has receiver = RSpec; bare
|
|
114
|
+
# `describe ... do` has nil receiver. Accept both.
|
|
115
|
+
node.receiver.nil? || describe_receiver?(node.receiver)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.describe_receiver?(receiver)
|
|
119
|
+
case receiver
|
|
120
|
+
when Prism::ConstantReadNode then receiver.name == :RSpec
|
|
121
|
+
when Prism::ConstantPathNode then receiver.name == :RSpec && receiver.parent.nil?
|
|
122
|
+
else false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.describe_call_const(describe_call)
|
|
127
|
+
first_arg = describe_call.arguments&.arguments&.first
|
|
128
|
+
render_const(first_arg)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.render_const(node)
|
|
132
|
+
case node
|
|
133
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
134
|
+
when Prism::ConstantPathNode
|
|
135
|
+
parts = []
|
|
136
|
+
current = node
|
|
137
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
138
|
+
parts.unshift(current.name.to_s)
|
|
139
|
+
current = current.parent
|
|
140
|
+
end
|
|
141
|
+
case current
|
|
142
|
+
when nil then "::#{parts.join('::')}"
|
|
143
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Walks the describe's body shallowly: collects every
|
|
149
|
+
# top-level `let(:name) { ... }` / `subject(:name) { ... }`
|
|
150
|
+
# / `subject { ... }` declaration. Does NOT recurse into
|
|
151
|
+
# nested describe / context blocks — those have their
|
|
152
|
+
# own record built by `collect` above.
|
|
153
|
+
def self.collect_lets(body)
|
|
154
|
+
return {} unless body.is_a?(Prism::Node)
|
|
155
|
+
|
|
156
|
+
lets = {}
|
|
157
|
+
statements_of(body).each do |stmt|
|
|
158
|
+
decl = let_declaration(stmt)
|
|
159
|
+
next if decl.nil?
|
|
160
|
+
|
|
161
|
+
lets[decl[:name]] = decl[:block]
|
|
162
|
+
end
|
|
163
|
+
lets.freeze
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.statements_of(node)
|
|
167
|
+
case node
|
|
168
|
+
when Prism::StatementsNode then node.body
|
|
169
|
+
else [node]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.let_declaration(node)
|
|
174
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
175
|
+
return nil unless node.receiver.nil?
|
|
176
|
+
return nil unless %i[let let! subject].include?(node.name)
|
|
177
|
+
return nil unless node.block.is_a?(Prism::BlockNode)
|
|
178
|
+
|
|
179
|
+
name = let_declaration_name(node)
|
|
180
|
+
return nil if name.nil?
|
|
181
|
+
|
|
182
|
+
{ name: name, block: node.block }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.let_declaration_name(call_node)
|
|
186
|
+
first_arg = call_node.arguments&.arguments&.first
|
|
187
|
+
if first_arg.nil?
|
|
188
|
+
# `subject { ... }` with no name — implicit :subject.
|
|
189
|
+
return :subject if call_node.name == :subject
|
|
190
|
+
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
return nil unless first_arg.is_a?(Prism::SymbolNode)
|
|
194
|
+
|
|
195
|
+
first_arg.unescaped.to_sym
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "rigor/type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Plugin
|
|
8
|
+
class Rspec < Rigor::Plugin::Base
|
|
9
|
+
# Pillar 2 Slice 2 — resolves the runtime type of a
|
|
10
|
+
# `let(:name) { body }` or `subject(:name) { body }`
|
|
11
|
+
# block by pattern-matching its body's tail expression.
|
|
12
|
+
#
|
|
13
|
+
# Recognised body shapes (floor):
|
|
14
|
+
#
|
|
15
|
+
# - `ConstantClass.new(...)` → `Nominal[ConstantClass]`
|
|
16
|
+
# - `Foo::Bar.new(...)` → `Nominal[Foo::Bar]`
|
|
17
|
+
# - `described_class.new(...)` → `Nominal[<describe const>]`
|
|
18
|
+
# when the surrounding describe block has a constant
|
|
19
|
+
# anchor.
|
|
20
|
+
# - `create(:factory)` / `build(:factory)` /
|
|
21
|
+
# `build_stubbed(:factory)` / `attributes_for(:factory)`
|
|
22
|
+
# → `Nominal[<factory.model_class>]` when the
|
|
23
|
+
# `:factory_index` ADR-9 fact is available and the
|
|
24
|
+
# factory name is known.
|
|
25
|
+
# - `FactoryBot.create(:factory)` etc. — same as the
|
|
26
|
+
# implicit-receiver factorybot form.
|
|
27
|
+
#
|
|
28
|
+
# Returns `nil` for any other body shape. Multi-statement
|
|
29
|
+
# bodies use the LAST statement (the block's return
|
|
30
|
+
# value).
|
|
31
|
+
module LetTypeResolver
|
|
32
|
+
FACTORYBOT_ENTRY_METHODS = %i[
|
|
33
|
+
create build build_stubbed attributes_for
|
|
34
|
+
create_list build_list build_stubbed_list
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param block_node [Prism::BlockNode]
|
|
40
|
+
# @param describe_const [String, nil]
|
|
41
|
+
# @param factory_index [Object, nil] something
|
|
42
|
+
# responding to `find(factory_name)` and returning
|
|
43
|
+
# an entry that responds to `model_class`.
|
|
44
|
+
# @param environment [Rigor::Environment, nil]
|
|
45
|
+
# @return [Rigor::Type, nil]
|
|
46
|
+
def resolve(block_node, describe_const:, factory_index:, environment:)
|
|
47
|
+
tail = body_tail(block_node)
|
|
48
|
+
return nil if tail.nil?
|
|
49
|
+
|
|
50
|
+
nominal_for_call(
|
|
51
|
+
tail,
|
|
52
|
+
describe_const: describe_const,
|
|
53
|
+
factory_index: factory_index,
|
|
54
|
+
environment: environment
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def body_tail(block_node)
|
|
59
|
+
body = block_node.body
|
|
60
|
+
return nil if body.nil?
|
|
61
|
+
|
|
62
|
+
case body
|
|
63
|
+
when Prism::StatementsNode
|
|
64
|
+
body.body.last
|
|
65
|
+
else
|
|
66
|
+
body
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def nominal_for_call(node, describe_const:, factory_index:, environment:)
|
|
71
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
72
|
+
|
|
73
|
+
# `<Const>.new(...)` / `<ConstPath>.new(...)`
|
|
74
|
+
if node.name == :new && const_receiver?(node.receiver)
|
|
75
|
+
class_name = render_const(node.receiver)
|
|
76
|
+
return nominal_for(class_name, environment: environment) if class_name
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `described_class.new(...)`
|
|
80
|
+
if node.name == :new && described_class_receiver?(node.receiver)
|
|
81
|
+
return nil if describe_const.nil?
|
|
82
|
+
|
|
83
|
+
return nominal_for(describe_const, environment: environment)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `create(:name)` / `build(:name)` / `FactoryBot.create(:name)` / `*_list`
|
|
87
|
+
factorybot_nominal(node, factory_index: factory_index, environment: environment)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def factorybot_nominal(call_node, factory_index:, environment:)
|
|
91
|
+
return nil if factory_index.nil?
|
|
92
|
+
return nil unless factorybot_call?(call_node)
|
|
93
|
+
|
|
94
|
+
factory_name = factorybot_first_arg(call_node)
|
|
95
|
+
return nil if factory_name.nil?
|
|
96
|
+
|
|
97
|
+
entry = factory_index.find(factory_name)
|
|
98
|
+
return nil if entry.nil?
|
|
99
|
+
|
|
100
|
+
model_class = entry.respond_to?(:model_class) ? entry.model_class : nil
|
|
101
|
+
return nil if model_class.nil?
|
|
102
|
+
|
|
103
|
+
nominal_for(model_class, environment: environment)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Recognises both `create(:name)` (implicit receiver,
|
|
107
|
+
# via `include FactoryBot::Syntax::Methods`) and the
|
|
108
|
+
# explicit `FactoryBot.create(:name)` /
|
|
109
|
+
# `FactoryGirl.create(:name)` (legacy) forms.
|
|
110
|
+
def factorybot_call?(call_node)
|
|
111
|
+
return false unless FACTORYBOT_ENTRY_METHODS.include?(call_node.name)
|
|
112
|
+
|
|
113
|
+
recv = call_node.receiver
|
|
114
|
+
recv.nil? || factorybot_constant?(recv)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def factorybot_constant?(node)
|
|
118
|
+
case node
|
|
119
|
+
when Prism::ConstantReadNode then %i[FactoryBot FactoryGirl].include?(node.name)
|
|
120
|
+
when Prism::ConstantPathNode
|
|
121
|
+
node.parent.nil? && %i[FactoryBot FactoryGirl].include?(node.name)
|
|
122
|
+
else false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def factorybot_first_arg(call_node)
|
|
127
|
+
first_arg = call_node.arguments&.arguments&.first
|
|
128
|
+
case first_arg
|
|
129
|
+
when Prism::SymbolNode then first_arg.unescaped
|
|
130
|
+
when Prism::StringNode then first_arg.unescaped
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def const_receiver?(node)
|
|
135
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def described_class_receiver?(node)
|
|
139
|
+
node.is_a?(Prism::CallNode) && node.name == :described_class && node.receiver.nil?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_const(node)
|
|
143
|
+
case node
|
|
144
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
145
|
+
when Prism::ConstantPathNode
|
|
146
|
+
parts = []
|
|
147
|
+
current = node
|
|
148
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
149
|
+
parts.unshift(current.name.to_s)
|
|
150
|
+
current = current.parent
|
|
151
|
+
end
|
|
152
|
+
case current
|
|
153
|
+
when nil then "::#{parts.join('::')}"
|
|
154
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def nominal_for(class_name, environment:)
|
|
160
|
+
if environment
|
|
161
|
+
environment.nominal_for_name(class_name) ||
|
|
162
|
+
Rigor::Type::Combinator.nominal_of(class_name)
|
|
163
|
+
else
|
|
164
|
+
Rigor::Type::Combinator.nominal_of(class_name)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|