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,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Rspec < Rigor::Plugin::Base
|
|
8
|
+
# Walks an RSpec spec file's AST and yields, for each
|
|
9
|
+
# describe / context block (including the outer
|
|
10
|
+
# `RSpec.describe`), a `Scope` value with the `let`
|
|
11
|
+
# and `subject` declarations recorded inside that
|
|
12
|
+
# scope.
|
|
13
|
+
#
|
|
14
|
+
# Scope hierarchy is preserved: each `Scope` carries
|
|
15
|
+
# a list of its `nested_scopes`. The analyzer uses the
|
|
16
|
+
# hierarchy to detect cross-scope shadowing.
|
|
17
|
+
#
|
|
18
|
+
# Recognised scope methods (when called without a
|
|
19
|
+
# receiver, or with `RSpec` as the receiver):
|
|
20
|
+
#
|
|
21
|
+
# - `describe` / `context` — both open a new nested
|
|
22
|
+
# scope.
|
|
23
|
+
# - `RSpec.describe` — the outermost scope.
|
|
24
|
+
#
|
|
25
|
+
# Recognised declaration methods (inside any scope,
|
|
26
|
+
# called without a receiver):
|
|
27
|
+
#
|
|
28
|
+
# - `let(:name) { ... }` — caches the block result
|
|
29
|
+
# per-example; recorded as a Declaration.
|
|
30
|
+
# - `let!(:name) { ... }` — same, but evaluated
|
|
31
|
+
# eagerly via a `before` hook; same shape for our
|
|
32
|
+
# purposes.
|
|
33
|
+
# - `subject(:name) { ... }` — special-cases name
|
|
34
|
+
# `:subject` when called without a name.
|
|
35
|
+
module ScopeWalker
|
|
36
|
+
SCOPE_METHODS = %i[describe context].freeze
|
|
37
|
+
DECLARATION_METHODS = %i[let let! subject].freeze
|
|
38
|
+
|
|
39
|
+
# @!attribute [r] kind
|
|
40
|
+
# `:describe`, `:context`, or `:rspec_describe` for
|
|
41
|
+
# the root.
|
|
42
|
+
# @!attribute [r] declarations
|
|
43
|
+
# `Array<Declaration>` declared in this scope.
|
|
44
|
+
# @!attribute [r] nested_scopes
|
|
45
|
+
# `Array<Scope>` nested under this scope.
|
|
46
|
+
# @!attribute [r] location
|
|
47
|
+
# `Prism::Location` of the call node that opened
|
|
48
|
+
# this scope.
|
|
49
|
+
Scope = Struct.new(:kind, :declarations, :nested_scopes, :location, keyword_init: true)
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] name
|
|
52
|
+
# `Symbol` declared name (`:user`, `:subject`,
|
|
53
|
+
# ...).
|
|
54
|
+
# @!attribute [r] kind
|
|
55
|
+
# `:let`, `:let!`, or `:subject`.
|
|
56
|
+
# @!attribute [r] location
|
|
57
|
+
# `Prism::Location` of the call node.
|
|
58
|
+
# @!attribute [r] block_node
|
|
59
|
+
# `Prism::BlockNode` of the declaration's body.
|
|
60
|
+
Declaration = Struct.new(:name, :kind, :location, :block_node, keyword_init: true)
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Walks the parsed file and returns an array of
|
|
65
|
+
# top-level scopes (each `RSpec.describe` is a
|
|
66
|
+
# separate root). Files with no recognised scopes
|
|
67
|
+
# return an empty array.
|
|
68
|
+
def collect_scopes(root)
|
|
69
|
+
scopes = []
|
|
70
|
+
walk_top_level(root, scopes)
|
|
71
|
+
scopes
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Walks every scope in a tree (root + descendants)
|
|
75
|
+
# and yields each in turn.
|
|
76
|
+
def each_scope(scope, &)
|
|
77
|
+
yield scope
|
|
78
|
+
scope.nested_scopes.each { |child| each_scope(child, &) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def walk_top_level(node, scopes)
|
|
82
|
+
return unless node.is_a?(Prism::Node)
|
|
83
|
+
|
|
84
|
+
if rspec_describe_call?(node)
|
|
85
|
+
scopes << build_scope(node, kind: :rspec_describe)
|
|
86
|
+
else
|
|
87
|
+
node.compact_child_nodes.each { |child| walk_top_level(child, scopes) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns true for `RSpec.describe ... do |...| ...
|
|
92
|
+
# end` calls.
|
|
93
|
+
def rspec_describe_call?(node)
|
|
94
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
95
|
+
return false unless node.name == :describe
|
|
96
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
97
|
+
|
|
98
|
+
receiver_name = constant_name(node.receiver)
|
|
99
|
+
%w[RSpec ::RSpec].include?(receiver_name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns true for `describe ... do ... end` /
|
|
103
|
+
# `context ... do ... end` (called without an
|
|
104
|
+
# explicit receiver — `RSpec.describe` is handled
|
|
105
|
+
# separately by `rspec_describe_call?`).
|
|
106
|
+
def nested_scope_call?(node)
|
|
107
|
+
node.is_a?(Prism::CallNode) &&
|
|
108
|
+
SCOPE_METHODS.include?(node.name) &&
|
|
109
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
110
|
+
node.receiver.nil?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def declaration_call?(node)
|
|
114
|
+
node.is_a?(Prism::CallNode) &&
|
|
115
|
+
DECLARATION_METHODS.include?(node.name) &&
|
|
116
|
+
node.receiver.nil?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Constructs a Scope from a describe / context /
|
|
120
|
+
# RSpec.describe call node. Walks the block body
|
|
121
|
+
# for declarations + nested scopes.
|
|
122
|
+
def build_scope(call_node, kind:)
|
|
123
|
+
declarations = []
|
|
124
|
+
nested = []
|
|
125
|
+
(call_node.block.body&.compact_child_nodes || []).each do |child|
|
|
126
|
+
classify_child(child, declarations, nested)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
Scope.new(
|
|
130
|
+
kind: kind,
|
|
131
|
+
declarations: declarations,
|
|
132
|
+
nested_scopes: nested,
|
|
133
|
+
location: call_node.location
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def classify_child(child, declarations, nested)
|
|
138
|
+
if declaration_call?(child)
|
|
139
|
+
decl = build_declaration(child)
|
|
140
|
+
declarations << decl if decl
|
|
141
|
+
elsif nested_scope_call?(child)
|
|
142
|
+
nested << build_scope(child, kind: child.name)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_declaration(call_node)
|
|
147
|
+
first_arg = call_node.arguments&.arguments&.first
|
|
148
|
+
# `subject(&block)` (no name) defaults to the
|
|
149
|
+
# implicit subject; record it as `:subject`.
|
|
150
|
+
if first_arg.nil?
|
|
151
|
+
return nil unless call_node.name == :subject
|
|
152
|
+
|
|
153
|
+
return Declaration.new(
|
|
154
|
+
name: :subject,
|
|
155
|
+
kind: call_node.name,
|
|
156
|
+
location: call_node.location,
|
|
157
|
+
block_node: call_node.block
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
return nil unless first_arg.is_a?(Prism::SymbolNode)
|
|
161
|
+
|
|
162
|
+
Declaration.new(
|
|
163
|
+
name: first_arg.unescaped.to_sym,
|
|
164
|
+
kind: call_node.name,
|
|
165
|
+
location: call_node.location,
|
|
166
|
+
block_node: call_node.block
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def constant_name(node)
|
|
171
|
+
case node
|
|
172
|
+
when nil then nil
|
|
173
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
174
|
+
when Prism::ConstantPathNode
|
|
175
|
+
parts = []
|
|
176
|
+
current = node
|
|
177
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
178
|
+
parts.unshift(current.name.to_s)
|
|
179
|
+
current = current.parent
|
|
180
|
+
end
|
|
181
|
+
case current
|
|
182
|
+
when nil then "::#{parts.join('::')}"
|
|
183
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
require_relative "rspec/scope_walker"
|
|
6
|
+
require_relative "rspec/analyzer"
|
|
7
|
+
require_relative "rspec/matcher_analyzer"
|
|
8
|
+
require_relative "rspec/let_scope_index"
|
|
9
|
+
require_relative "rspec/let_type_resolver"
|
|
10
|
+
|
|
11
|
+
module Rigor
|
|
12
|
+
module Plugin
|
|
13
|
+
# rigor-rspec — validates RSpec `let` / `subject`
|
|
14
|
+
# declarations within each describe / context scope.
|
|
15
|
+
#
|
|
16
|
+
# Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
17
|
+
# Deliberately scoped — the roadmap describes a much
|
|
18
|
+
# larger plugin (let-typo detection in `it` bodies,
|
|
19
|
+
# `expect(x).to receive(:method)` mock-target
|
|
20
|
+
# validation). Both are out of scope for v0.1.0; this
|
|
21
|
+
# plugin ships the two checks that have the lowest
|
|
22
|
+
# false-positive risk:
|
|
23
|
+
#
|
|
24
|
+
# 1. **Duplicate `let` / `subject` declarations** in
|
|
25
|
+
# the same scope (`warning`). RSpec's runtime lets
|
|
26
|
+
# the last declaration win, so the first one is
|
|
27
|
+
# silently shadowed — almost always a copy-paste
|
|
28
|
+
# bug.
|
|
29
|
+
# 2. **Self-referencing `let` / `subject`** — calling
|
|
30
|
+
# the declared name *inside* its own block body
|
|
31
|
+
# (`error`). At runtime this infinite-loops; users
|
|
32
|
+
# typically meant to call a different method or
|
|
33
|
+
# forgot to introduce a `super`.
|
|
34
|
+
#
|
|
35
|
+
# ## Configuration
|
|
36
|
+
#
|
|
37
|
+
# No knobs in v0.1.0. The plugin walks every analysed
|
|
38
|
+
# file looking for `RSpec.describe ... do` blocks; spec
|
|
39
|
+
# files outside the project's `paths:` are not scanned.
|
|
40
|
+
#
|
|
41
|
+
# ## Limitations (v0.1.0)
|
|
42
|
+
#
|
|
43
|
+
# - **No let-typo detection.** Detecting an `it`
|
|
44
|
+
# block's reference to a misspelled `let` name
|
|
45
|
+
# requires resolving every method call inside the
|
|
46
|
+
# block against the let scope chain, the included
|
|
47
|
+
# modules, the matchers DSL, and helper methods.
|
|
48
|
+
# Reliable diagnostics here need a much heavier
|
|
49
|
+
# walker — see the README's `Future direction`.
|
|
50
|
+
# - **No mock-target validation.**
|
|
51
|
+
# `expect(x).to receive(:nme)` validating against
|
|
52
|
+
# `x`'s methods is a separate slice; it overlaps with
|
|
53
|
+
# the engine's general method-existence
|
|
54
|
+
# diagnostics and needs careful coordination to avoid
|
|
55
|
+
# double-firing.
|
|
56
|
+
# - **No shared-context resolution.** `include_context`,
|
|
57
|
+
# `shared_context`, and `it_behaves_like` are
|
|
58
|
+
# recognised as scope-opening calls but their
|
|
59
|
+
# declarations are not pulled into the host scope.
|
|
60
|
+
# - **Constant validation is not done here.**
|
|
61
|
+
# `RSpec.describe SomeClass do` does not validate
|
|
62
|
+
# `SomeClass`; the engine's `inference.unresolved-constant`
|
|
63
|
+
# already catches that.
|
|
64
|
+
class Rspec < Rigor::Plugin::Base
|
|
65
|
+
manifest(
|
|
66
|
+
id: "rspec",
|
|
67
|
+
version: "0.3.0",
|
|
68
|
+
description: "Validates RSpec `let` / `subject` declarations within each scope; " \
|
|
69
|
+
"narrows expect(x).to <matcher> assertions downstream in `it` bodies; " \
|
|
70
|
+
"binds let / subject locals to their inferred return type (Pillar 2 " \
|
|
71
|
+
"Slice 2) — `let(:user) { User.new(...) }` / `let(:user) { create(:user) }` / " \
|
|
72
|
+
"`subject { described_class.new(...) }`.",
|
|
73
|
+
consumes: [
|
|
74
|
+
{ plugin_id: "factorybot", name: :factory_index, optional: true }
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def init(services)
|
|
79
|
+
@services = services
|
|
80
|
+
@factory_index_resolved = false
|
|
81
|
+
@factory_index = nil
|
|
82
|
+
# Per-path `LetScopeIndex` cache. The plugin's
|
|
83
|
+
# `flow_contribution_for` is called for every call
|
|
84
|
+
# node the dispatcher visits; building the index once
|
|
85
|
+
# per file is essential for performance.
|
|
86
|
+
@let_index_cache = {}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
90
|
+
# Build the let-scope index for this file while we
|
|
91
|
+
# have the parsed root in hand — `flow_contribution_for`
|
|
92
|
+
# picks it up from `@let_index_cache` keyed on path.
|
|
93
|
+
@let_index_cache[path] ||= LetScopeIndex.build(root)
|
|
94
|
+
Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Pillar 2 Slice 1 — spec-derived flow facts from RSpec
|
|
98
|
+
# matcher assertions (six-matcher floor: `be_a`,
|
|
99
|
+
# `be_kind_of`, `be_instance_of`, `be_nil`, `eq(literal)`,
|
|
100
|
+
# `eql(literal)`; `match(/regex/)`; `not_to` / `to_not`).
|
|
101
|
+
#
|
|
102
|
+
# Pillar 2 Slice 2 (v0.3.0) — additionally binds local
|
|
103
|
+
# reads in `it` / spec bodies to their `let(:name) { ... }`
|
|
104
|
+
# block's inferred return type. Composes with Slice 1:
|
|
105
|
+
# a matcher narrowing fires after the let binding.
|
|
106
|
+
def flow_contribution_for(call_node:, scope:)
|
|
107
|
+
matcher = MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)
|
|
108
|
+
return matcher if matcher
|
|
109
|
+
|
|
110
|
+
let_binding_contribution(call_node, scope)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Pillar 2 Slice 2 — when the call node is a no-receiver
|
|
116
|
+
# method call (`user`, `subject`, etc.) inside an RSpec
|
|
117
|
+
# `describe` block whose lets include a matching name,
|
|
118
|
+
# return a `FlowContribution(return_type: <inferred>)`.
|
|
119
|
+
def let_binding_contribution(call_node, scope)
|
|
120
|
+
return nil if scope.nil?
|
|
121
|
+
return nil unless candidate_call?(call_node)
|
|
122
|
+
|
|
123
|
+
index = let_scope_index_for(scope.source_path)
|
|
124
|
+
return nil if index.nil?
|
|
125
|
+
|
|
126
|
+
line = call_node.location.start_line
|
|
127
|
+
block_node = index.let_block_at(line, call_node.name)
|
|
128
|
+
return nil if block_node.nil?
|
|
129
|
+
|
|
130
|
+
describe_const = index.describe_const_at(line)
|
|
131
|
+
type = LetTypeResolver.resolve(
|
|
132
|
+
block_node,
|
|
133
|
+
describe_const: describe_const,
|
|
134
|
+
factory_index: factory_index_or_nil,
|
|
135
|
+
environment: scope.environment
|
|
136
|
+
)
|
|
137
|
+
return nil if type.nil?
|
|
138
|
+
|
|
139
|
+
Rigor::FlowContribution.new(return_type: type)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def candidate_call?(call_node)
|
|
143
|
+
call_node.is_a?(Prism::CallNode) &&
|
|
144
|
+
call_node.receiver.nil? &&
|
|
145
|
+
call_node.block.nil? &&
|
|
146
|
+
# Calls with arguments are matcher / DSL invocations,
|
|
147
|
+
# not let-bound name reads. `subject` / `user` etc.
|
|
148
|
+
# without args are the implicit-method-call shape RSpec
|
|
149
|
+
# uses to expose let / subject in `it` bodies.
|
|
150
|
+
(call_node.arguments.nil? || call_node.arguments.arguments.empty?)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def let_scope_index_for(path)
|
|
154
|
+
return nil if path.nil?
|
|
155
|
+
return @let_index_cache[path] if @let_index_cache.key?(path)
|
|
156
|
+
|
|
157
|
+
@let_index_cache[path] = build_let_scope_index(path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def build_let_scope_index(path)
|
|
161
|
+
source = io_boundary.read_file(path)
|
|
162
|
+
parse_result = Prism.parse(source)
|
|
163
|
+
return nil unless parse_result.errors.empty?
|
|
164
|
+
|
|
165
|
+
LetScopeIndex.build(parse_result.value)
|
|
166
|
+
rescue Rigor::Plugin::AccessDeniedError, Errno::ENOENT
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def factory_index_or_nil
|
|
171
|
+
return @factory_index if @factory_index_resolved
|
|
172
|
+
|
|
173
|
+
@factory_index = @services&.fact_store&.read(plugin_id: "factorybot", name: :factory_index)
|
|
174
|
+
@factory_index_resolved = true
|
|
175
|
+
@factory_index
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def build_diagnostic(diag)
|
|
179
|
+
Rigor::Analysis::Diagnostic.new(
|
|
180
|
+
path: diag.path, line: diag.line, column: diag.column,
|
|
181
|
+
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
Rigor::Plugin.register(Rspec)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "http_status_codes"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class RspecRails < Rigor::Plugin::Base
|
|
10
|
+
# Validates `expect(response).to have_http_status(arg)` /
|
|
11
|
+
# the negated `.not_to have_http_status(arg)` and the
|
|
12
|
+
# bare matcher form `should have_http_status(arg)`.
|
|
13
|
+
#
|
|
14
|
+
# Recognised argument shapes:
|
|
15
|
+
#
|
|
16
|
+
# - **IntegerNode**: must be in `100..599`. Numbers outside
|
|
17
|
+
# that range fire `have_http_status.out-of-range`.
|
|
18
|
+
# - **SymbolNode**: must be one of the known
|
|
19
|
+
# `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys OR a Rails
|
|
20
|
+
# status-group alias (`:success` / `:successful` /
|
|
21
|
+
# `:missing` / `:redirect` / `:error` / `:client_error` /
|
|
22
|
+
# `:server_error` / `:informational`). Unknown symbols
|
|
23
|
+
# fire `have_http_status.unknown-symbol` (typo-flavoured).
|
|
24
|
+
# - **StringNode**: passed through silently (a literal
|
|
25
|
+
# `"200"` is accepted by Rails at runtime; we don't
|
|
26
|
+
# second-guess the user's intent for the String form).
|
|
27
|
+
# - **Anything else** (variable, method call, computed
|
|
28
|
+
# expression): skipped — the plugin can't statically
|
|
29
|
+
# prove the runtime value.
|
|
30
|
+
#
|
|
31
|
+
# Walks every `Prism::CallNode` looking for matcher
|
|
32
|
+
# invocations rather than only the `expect(...).to ...`
|
|
33
|
+
# chain because `have_http_status` is also used as a
|
|
34
|
+
# plain matcher inside Rails' `assert_response` shim and
|
|
35
|
+
# in shared-context bodies; the diagnostic is the same
|
|
36
|
+
# regardless of the surrounding chain.
|
|
37
|
+
module HaveHttpStatusAnalyzer
|
|
38
|
+
Diagnostic = Struct.new(:path, :line, :column, :severity, :rule, :message, keyword_init: true)
|
|
39
|
+
|
|
40
|
+
MATCHER_NAME = :have_http_status
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# @param path [String]
|
|
45
|
+
# @param root [Prism::Node]
|
|
46
|
+
# @return [Array<Diagnostic>]
|
|
47
|
+
def diagnose(path:, root:)
|
|
48
|
+
diagnostics = []
|
|
49
|
+
walk(root) do |call_node|
|
|
50
|
+
diagnostic = diagnostic_for(call_node, path)
|
|
51
|
+
diagnostics << diagnostic if diagnostic
|
|
52
|
+
end
|
|
53
|
+
diagnostics
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def walk(node, &)
|
|
57
|
+
return unless node.is_a?(Prism::Node)
|
|
58
|
+
|
|
59
|
+
yield node if call_to_matcher?(node)
|
|
60
|
+
node.compact_child_nodes.each { |child| walk(child, &) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `have_http_status` is a matcher — called either with
|
|
64
|
+
# no receiver (`have_http_status(200)` inside `.to(...)`)
|
|
65
|
+
# or with a receiver chain we don't care about here.
|
|
66
|
+
# Either way we want the call node whose name is
|
|
67
|
+
# `have_http_status` and that carries exactly one
|
|
68
|
+
# positional argument.
|
|
69
|
+
def call_to_matcher?(node)
|
|
70
|
+
node.is_a?(Prism::CallNode) &&
|
|
71
|
+
node.name == MATCHER_NAME &&
|
|
72
|
+
single_positional?(node)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def single_positional?(call_node)
|
|
76
|
+
args = call_node.arguments&.arguments || []
|
|
77
|
+
args.size == 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def diagnostic_for(call_node, path)
|
|
81
|
+
arg = call_node.arguments.arguments.first
|
|
82
|
+
case arg
|
|
83
|
+
when Prism::IntegerNode then integer_diagnostic(call_node, path, arg)
|
|
84
|
+
when Prism::SymbolNode then symbol_diagnostic(call_node, path, arg)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def integer_diagnostic(call_node, path, integer_node)
|
|
89
|
+
value = integer_node.value
|
|
90
|
+
return nil if HttpStatusCodes::VALID_NUMERIC_RANGE.cover?(value)
|
|
91
|
+
|
|
92
|
+
build_diagnostic(
|
|
93
|
+
call_node, path,
|
|
94
|
+
rule: "have_http_status.out-of-range",
|
|
95
|
+
message: "have_http_status(#{value}) is outside the valid HTTP status " \
|
|
96
|
+
"range #{HttpStatusCodes::VALID_NUMERIC_RANGE}"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def symbol_diagnostic(call_node, path, symbol_node)
|
|
101
|
+
sym = symbol_node.unescaped.to_sym
|
|
102
|
+
return nil if HttpStatusCodes::KNOWN_SYMBOLS.include?(sym)
|
|
103
|
+
|
|
104
|
+
build_diagnostic(
|
|
105
|
+
call_node, path,
|
|
106
|
+
rule: "have_http_status.unknown-symbol",
|
|
107
|
+
message: "have_http_status(:#{sym}) is not a recognised HTTP status symbol " \
|
|
108
|
+
"(see Rack::Utils::SYMBOL_TO_STATUS_CODE) or Rails status-group " \
|
|
109
|
+
"alias (:success / :successful / :missing / :redirect / :error / " \
|
|
110
|
+
":client_error / :server_error / :informational)"
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_diagnostic(call_node, path, rule:, message:)
|
|
115
|
+
location = call_node.message_loc || call_node.location
|
|
116
|
+
Diagnostic.new(
|
|
117
|
+
path: path,
|
|
118
|
+
line: location.start_line,
|
|
119
|
+
column: location.start_column + 1,
|
|
120
|
+
severity: :warning,
|
|
121
|
+
rule: rule,
|
|
122
|
+
message: message
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class RspecRails < Rigor::Plugin::Base
|
|
6
|
+
# Curated set of HTTP-status symbols `have_http_status`
|
|
7
|
+
# accepts. Derived from `Rack::Utils::SYMBOL_TO_STATUS_CODE`
|
|
8
|
+
# (Rack 3.x snapshot) plus Rails' status-group aliases
|
|
9
|
+
# accepted by `ActionDispatch::TestResponse`.
|
|
10
|
+
#
|
|
11
|
+
# Vendored as a frozen Set so the plugin has no
|
|
12
|
+
# `rack` / `actionpack` runtime dependency — checking
|
|
13
|
+
# against a static catalogue is intentional: a future
|
|
14
|
+
# Rack release that adds a new alias would require
|
|
15
|
+
# bumping this constant, but the false-positive risk is
|
|
16
|
+
# bounded (the diagnostic just suggests a typo when in
|
|
17
|
+
# fact the symbol is newly-added).
|
|
18
|
+
module HttpStatusCodes
|
|
19
|
+
SYMBOL_TO_STATUS_CODE = {
|
|
20
|
+
# 1xx Informational
|
|
21
|
+
continue: 100, switching_protocols: 101, processing: 102, early_hints: 103,
|
|
22
|
+
# 2xx Success
|
|
23
|
+
ok: 200, created: 201, accepted: 202, non_authoritative_information: 203,
|
|
24
|
+
no_content: 204, reset_content: 205, partial_content: 206, multi_status: 207,
|
|
25
|
+
already_reported: 208, im_used: 226,
|
|
26
|
+
# 3xx Redirection
|
|
27
|
+
multiple_choices: 300, moved_permanently: 301, found: 302, see_other: 303,
|
|
28
|
+
not_modified: 304, use_proxy: 305, temporary_redirect: 307,
|
|
29
|
+
permanent_redirect: 308,
|
|
30
|
+
# 4xx Client Error
|
|
31
|
+
bad_request: 400, unauthorized: 401, payment_required: 402, forbidden: 403,
|
|
32
|
+
not_found: 404, method_not_allowed: 405, not_acceptable: 406,
|
|
33
|
+
proxy_authentication_required: 407, request_timeout: 408, conflict: 409,
|
|
34
|
+
gone: 410, length_required: 411, precondition_failed: 412,
|
|
35
|
+
payload_too_large: 413, uri_too_long: 414, unsupported_media_type: 415,
|
|
36
|
+
range_not_satisfiable: 416, expectation_failed: 417, misdirected_request: 421,
|
|
37
|
+
unprocessable_entity: 422, unprocessable_content: 422, locked: 423,
|
|
38
|
+
failed_dependency: 424, too_early: 425, upgrade_required: 426,
|
|
39
|
+
precondition_required: 428, too_many_requests: 429,
|
|
40
|
+
request_header_fields_too_large: 431, unavailable_for_legal_reasons: 451,
|
|
41
|
+
# 5xx Server Error
|
|
42
|
+
internal_server_error: 500, not_implemented: 501, bad_gateway: 502,
|
|
43
|
+
service_unavailable: 503, gateway_timeout: 504, http_version_not_supported: 505,
|
|
44
|
+
variant_also_negotiates: 506, insufficient_storage: 507, loop_detected: 508,
|
|
45
|
+
not_extended: 510, network_authentication_required: 511,
|
|
46
|
+
# Rails status-group aliases (see
|
|
47
|
+
# ActionDispatch::TestResponse RESPONSE_PREDICATES)
|
|
48
|
+
informational: :informational_group, success: :success_group,
|
|
49
|
+
successful: :success_group, redirect: :redirect_group,
|
|
50
|
+
client_error: :client_error_group, missing: :client_error_group,
|
|
51
|
+
server_error: :server_error_group, error: :server_error_group
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
KNOWN_SYMBOLS = SYMBOL_TO_STATUS_CODE.keys.to_set.freeze
|
|
55
|
+
|
|
56
|
+
VALID_NUMERIC_RANGE = (100..599)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
require_relative "rspec_rails/have_http_status_analyzer"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
# rigor-rspec-rails — validates rspec-rails matchers whose
|
|
10
|
+
# arguments are statically checkable.
|
|
11
|
+
#
|
|
12
|
+
# v0.1.0 covers `have_http_status(int_or_symbol)`:
|
|
13
|
+
#
|
|
14
|
+
# - Integer: must be in `100..599`.
|
|
15
|
+
# - Symbol: must be one of the
|
|
16
|
+
# `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys or one of
|
|
17
|
+
# Rails' status-group aliases (`:success`, `:successful`,
|
|
18
|
+
# `:missing`, `:redirect`, `:error`, `:client_error`,
|
|
19
|
+
# `:server_error`, `:informational`).
|
|
20
|
+
#
|
|
21
|
+
# ## Why this is a separate plugin from rigor-rspec
|
|
22
|
+
#
|
|
23
|
+
# `rigor-rspec` handles the **type-narrowing** matchers
|
|
24
|
+
# (`be_a`, `be_kind_of`, `be_nil`, `eq(literal)`, etc.) —
|
|
25
|
+
# ones that refine a local's static type so downstream
|
|
26
|
+
# calls in the same `it` body resolve at the narrowed
|
|
27
|
+
# type. `rigor-rspec-rails` handles the **behavioral**
|
|
28
|
+
# matchers — ones that assert runtime state (HTTP status,
|
|
29
|
+
# rendered template, route shape) without narrowing a
|
|
30
|
+
# type. The two are activated independently in `.rigor.yml`.
|
|
31
|
+
#
|
|
32
|
+
# ## Deferred matchers
|
|
33
|
+
#
|
|
34
|
+
# - `render_template(...)` — overlaps with
|
|
35
|
+
# `rigor-actionpack`'s render-target validation
|
|
36
|
+
# (`render :show` against the view tree). A separate
|
|
37
|
+
# slice would coordinate the two to avoid
|
|
38
|
+
# double-firing.
|
|
39
|
+
# - `route_to(...)` / `redirect_to(...)` — needs the
|
|
40
|
+
# routes table from `rigor-rails-routes`
|
|
41
|
+
# (`:helper_table` cross-plugin fact). Future slice.
|
|
42
|
+
# - `have_enqueued_job(JobClass)` /
|
|
43
|
+
# `have_enqueued_mail(MailerClass)` — class-existence
|
|
44
|
+
# check overlaps with engine's
|
|
45
|
+
# `inference.unresolved-constant`; queued behind a
|
|
46
|
+
# decision on which surface owns the diagnostic.
|
|
47
|
+
# - `have_received(:method)` — overlaps with engine's
|
|
48
|
+
# `call.undefined-method`; same coordination question.
|
|
49
|
+
# - `be_routable` — needs routes table.
|
|
50
|
+
# - `match_response_schema(...)` (rswag / OpenAPI) — out
|
|
51
|
+
# of scope, separate plugin.
|
|
52
|
+
class RspecRails < Rigor::Plugin::Base
|
|
53
|
+
manifest(
|
|
54
|
+
id: "rspec-rails",
|
|
55
|
+
version: "0.1.0",
|
|
56
|
+
description: "Validates rspec-rails behavioral matchers (have_http_status floor)."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
60
|
+
HaveHttpStatusAnalyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_diagnostic(diag)
|
|
66
|
+
Rigor::Analysis::Diagnostic.new(
|
|
67
|
+
path: diag.path, line: diag.line, column: diag.column,
|
|
68
|
+
message: diag.message, severity: diag.severity, rule: diag.rule
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
Rigor::Plugin.register(RspecRails)
|
|
74
|
+
end
|
|
75
|
+
end
|