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,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Plugin
|
|
7
|
+
class Activerecord < Rigor::Plugin::Base
|
|
8
|
+
# Per-file AST walker. For each `Model.find(...)` /
|
|
9
|
+
# `Model.find_by(...)` / `Model.where(...)` call where
|
|
10
|
+
# `Model` is a name in the {ModelIndex}, emits diagnostics:
|
|
11
|
+
#
|
|
12
|
+
# | Method | Recognised arg shape | Validation |
|
|
13
|
+
# | --- | --- | --- |
|
|
14
|
+
# | `Model.find(id)` | any positional | arity check (1+ args) |
|
|
15
|
+
# | `Model.find_by(col: v, ...)` | keyword args | each key must be a column |
|
|
16
|
+
# | `Model.where(col: v, ...)` | keyword args | each key must be a column |
|
|
17
|
+
# | `Model.where(string)` | String literal | parser-side; not validated |
|
|
18
|
+
#
|
|
19
|
+
# Successful matches surface as `:info` diagnostics naming
|
|
20
|
+
# the resolved table; unknown columns surface as `:error`.
|
|
21
|
+
# Calls whose receiver is not a model in the index, and
|
|
22
|
+
# calls with non-keyword arguments to `where` / `find_by`,
|
|
23
|
+
# stay silent.
|
|
24
|
+
class Analyzer
|
|
25
|
+
# Methods that take a column → value Hash and need each key
|
|
26
|
+
# validated against the receiver's column set. The bang
|
|
27
|
+
# variants (`find_by!` raises instead of returning nil;
|
|
28
|
+
# `find_or_create_by!` raises on a validation failure) and
|
|
29
|
+
# `create_or_find_by` / `create_or_find_by!` take the
|
|
30
|
+
# identical column-hash argument shape.
|
|
31
|
+
COLUMN_HASH_METHODS = %i[
|
|
32
|
+
where
|
|
33
|
+
find_by find_by!
|
|
34
|
+
find_or_create_by find_or_create_by!
|
|
35
|
+
find_or_initialize_by
|
|
36
|
+
create_or_find_by create_or_find_by!
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
DID_YOU_MEAN_DISTANCE = 3
|
|
40
|
+
|
|
41
|
+
attr_reader :diagnostics
|
|
42
|
+
|
|
43
|
+
def initialize(path:, model_index:)
|
|
44
|
+
@path = path
|
|
45
|
+
@model_index = model_index
|
|
46
|
+
@diagnostics = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def analyze(root)
|
|
50
|
+
walk(root) { |node| visit_call(node) if node.is_a?(Prism::CallNode) }
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def walk(node, &)
|
|
57
|
+
return if node.nil?
|
|
58
|
+
|
|
59
|
+
yield node
|
|
60
|
+
node.compact_child_nodes.each { |child| walk(child, &) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def visit_call(node)
|
|
64
|
+
model_name = constant_receiver_name(node.receiver)
|
|
65
|
+
return if model_name.nil?
|
|
66
|
+
|
|
67
|
+
entry = @model_index.find(model_name) ||
|
|
68
|
+
@model_index.find("::#{model_name}")
|
|
69
|
+
return if entry.nil?
|
|
70
|
+
|
|
71
|
+
case node.name
|
|
72
|
+
when :find then validate_find(node, entry)
|
|
73
|
+
when *COLUMN_HASH_METHODS then validate_column_hash_call(node, entry)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_find(node, entry)
|
|
78
|
+
arity = call_argument_count(node)
|
|
79
|
+
if arity.zero?
|
|
80
|
+
push_error(node, "wrong-arity",
|
|
81
|
+
"`#{entry.class_name}.find` expects at least 1 argument, got 0")
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
push_info(node, "model-call",
|
|
86
|
+
"`#{entry.class_name}.find` returns #{entry.class_name} (table: `#{entry.table_name}`)")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_column_hash_call(node, entry)
|
|
90
|
+
keyword_pairs = keyword_argument_pairs(node)
|
|
91
|
+
return push_recognised(node, entry) if keyword_pairs.empty?
|
|
92
|
+
|
|
93
|
+
# Models with no schema-side columns are virtual — backed
|
|
94
|
+
# by a database VIEW (Mastodon's `Instance` model wraps a
|
|
95
|
+
# SQL view that isn't in `db/schema.rb`), seeded from an
|
|
96
|
+
# external source, or otherwise opaque to our schema
|
|
97
|
+
# parser. Without a column set we cannot meaningfully
|
|
98
|
+
# check query keys; surface the call as recognised and
|
|
99
|
+
# skip column validation entirely rather than firing a
|
|
100
|
+
# false `unknown-column` against every key.
|
|
101
|
+
return push_recognised(node, entry, keyword_pairs.map { |p| p[:key] }) if entry.column_names.empty?
|
|
102
|
+
|
|
103
|
+
unknown = keyword_pairs.reject { |pair| valid_query_key?(entry, pair[:key]) }
|
|
104
|
+
if unknown.empty?
|
|
105
|
+
keyword_pairs.each { |pair| validate_enum_value(node, entry, pair) }
|
|
106
|
+
push_recognised(node, entry, keyword_pairs.map { |p| p[:key] })
|
|
107
|
+
else
|
|
108
|
+
unknown.each do |pair|
|
|
109
|
+
key = pair[:key]
|
|
110
|
+
suggestion = closest_column(key, entry.column_names)
|
|
111
|
+
hint = suggestion ? " (did you mean `:#{suggestion}`?)" : ""
|
|
112
|
+
push_error(node, "unknown-column",
|
|
113
|
+
"`#{entry.class_name}.#{node.name}(#{key}: ...)` references " \
|
|
114
|
+
"unknown column `#{key}` on table `#{entry.table_name}`#{hint}")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ActiveRecord's `where` / `find_by` / `find_or_initialize_by`
|
|
120
|
+
# accept either a column name *or* a singular association
|
|
121
|
+
# name (belongs_to / has_one). The latter resolves to the
|
|
122
|
+
# association's FK column behind the scenes:
|
|
123
|
+
#
|
|
124
|
+
# AccountPin.find_by(account: x)
|
|
125
|
+
# # → SELECT … WHERE account_id = x.id
|
|
126
|
+
#
|
|
127
|
+
# Without this allowance, every `find_by(<assoc>:)` call
|
|
128
|
+
# surfaces as an `unknown-column` false positive — Mastodon
|
|
129
|
+
# saw ~100 such hits across the API controllers, all of
|
|
130
|
+
# which were the canonical Rails idiom rather than typos.
|
|
131
|
+
#
|
|
132
|
+
# `alias_attribute` names resolve to their target before
|
|
133
|
+
# the column / association check — querying by an aliased
|
|
134
|
+
# attribute is a valid Rails idiom and must not surface as
|
|
135
|
+
# an `unknown-column`.
|
|
136
|
+
def valid_query_key?(entry, key)
|
|
137
|
+
key = entry.resolve_alias(key) if entry.alias?(key)
|
|
138
|
+
|
|
139
|
+
return true if entry.column?(key)
|
|
140
|
+
|
|
141
|
+
assoc = entry.association(key)
|
|
142
|
+
assoc && assoc[:kind] == :singular
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# When the column is an enum-bearing column AND the
|
|
146
|
+
# passed value is a Symbol literal, the value MUST be
|
|
147
|
+
# one of the declared enum values. Non-Symbol values
|
|
148
|
+
# (variables, expressions) decline so dynamic call
|
|
149
|
+
# sites stay silent. Sequences (`status: [:active,
|
|
150
|
+
# :archived]`) are intentionally NOT walked here —
|
|
151
|
+
# the relation-shape check belongs in a future track.
|
|
152
|
+
def validate_enum_value(node, entry, pair)
|
|
153
|
+
return unless entry.enum?(pair[:key])
|
|
154
|
+
return unless pair[:value].is_a?(Prism::SymbolNode)
|
|
155
|
+
|
|
156
|
+
value = pair[:value].unescaped
|
|
157
|
+
values = entry.enum_values(pair[:key])
|
|
158
|
+
return if values.include?(value)
|
|
159
|
+
|
|
160
|
+
suggestion = closest_column(value, values)
|
|
161
|
+
hint = suggestion ? " (did you mean `:#{suggestion}`?)" : ""
|
|
162
|
+
push_error(node, "unknown-enum-value",
|
|
163
|
+
"`#{entry.class_name}.#{node.name}(#{pair[:key]}: :#{value})` references " \
|
|
164
|
+
"unknown enum value `:#{value}` (declared: #{values.map { |v| ":#{v}" }.join(', ')})#{hint}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def push_recognised(node, entry, keys = nil)
|
|
168
|
+
msg = "`#{entry.class_name}.#{node.name}`"
|
|
169
|
+
msg += " (#{keys.map { |k| ":#{k}" }.join(', ')})" if keys && !keys.empty?
|
|
170
|
+
msg += " on table `#{entry.table_name}`"
|
|
171
|
+
push_info(node, "model-call", msg)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def constant_receiver_name(node)
|
|
175
|
+
case node
|
|
176
|
+
when Prism::ConstantReadNode
|
|
177
|
+
node.name.to_s
|
|
178
|
+
when Prism::ConstantPathNode
|
|
179
|
+
constant_path_name(node)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def constant_path_name(node)
|
|
184
|
+
parts = []
|
|
185
|
+
current = node
|
|
186
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
187
|
+
parts.unshift(current.name.to_s)
|
|
188
|
+
current = current.parent
|
|
189
|
+
end
|
|
190
|
+
case current
|
|
191
|
+
when nil
|
|
192
|
+
"::#{parts.join('::')}"
|
|
193
|
+
when Prism::ConstantReadNode
|
|
194
|
+
"#{current.name}::#{parts.join('::')}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def call_argument_count(node)
|
|
199
|
+
return 0 if node.arguments.nil?
|
|
200
|
+
|
|
201
|
+
node.arguments.arguments.size
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns the symbol-keyed pairs of any KeywordHashNode
|
|
205
|
+
# argument as `{ key:, value: }` rows. Plain hash-literal
|
|
206
|
+
# arguments (`Model.where({a: 1})`) are NOT walked —
|
|
207
|
+
# Rails accepts both, but the keyword form is the
|
|
208
|
+
# idiomatic one. The `value` Prism node is preserved so
|
|
209
|
+
# the enum check downstream can recognise Symbol
|
|
210
|
+
# literals without re-walking the call.
|
|
211
|
+
def keyword_argument_pairs(node)
|
|
212
|
+
return [] if node.arguments.nil?
|
|
213
|
+
|
|
214
|
+
pairs = []
|
|
215
|
+
node.arguments.arguments.each do |arg|
|
|
216
|
+
next unless arg.is_a?(Prism::KeywordHashNode)
|
|
217
|
+
|
|
218
|
+
arg.elements.each do |pair|
|
|
219
|
+
next unless pair.is_a?(Prism::AssocNode) && pair.key.is_a?(Prism::SymbolNode)
|
|
220
|
+
|
|
221
|
+
pairs << { key: pair.key.unescaped, value: pair.value }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
pairs
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def closest_column(name, candidates)
|
|
228
|
+
best = nil
|
|
229
|
+
best_distance = DID_YOU_MEAN_DISTANCE + 1
|
|
230
|
+
candidates.each do |candidate|
|
|
231
|
+
distance = levenshtein(name, candidate)
|
|
232
|
+
if distance < best_distance
|
|
233
|
+
best = candidate
|
|
234
|
+
best_distance = distance
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
best
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def levenshtein(a, b) # rubocop:disable Naming/MethodParameterName
|
|
241
|
+
return b.length if a.empty?
|
|
242
|
+
return a.length if b.empty?
|
|
243
|
+
|
|
244
|
+
rows = Array.new(a.length + 1) { |_i| Array.new(b.length + 1, 0) }
|
|
245
|
+
(0..a.length).each { |i| rows[i][0] = i }
|
|
246
|
+
(0..b.length).each { |j| rows[0][j] = j }
|
|
247
|
+
|
|
248
|
+
(1..a.length).each do |i|
|
|
249
|
+
(1..b.length).each do |j|
|
|
250
|
+
cost = a[i - 1] == b[j - 1] ? 0 : 1
|
|
251
|
+
rows[i][j] = [
|
|
252
|
+
rows[i - 1][j] + 1,
|
|
253
|
+
rows[i][j - 1] + 1,
|
|
254
|
+
rows[i - 1][j - 1] + cost
|
|
255
|
+
].min
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
rows[a.length][b.length]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def push_info(node, rule, message)
|
|
262
|
+
push_diagnostic(node, severity: :info, rule: rule, message: message)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def push_error(node, rule, message)
|
|
266
|
+
push_diagnostic(node, severity: :error, rule: rule, message: message)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def push_diagnostic(node, severity:, rule:, message:)
|
|
270
|
+
location = node.location
|
|
271
|
+
@diagnostics << Rigor::Analysis::Diagnostic.new(
|
|
272
|
+
path: @path,
|
|
273
|
+
line: location.start_line,
|
|
274
|
+
column: location.start_column + 1,
|
|
275
|
+
message: message,
|
|
276
|
+
severity: severity,
|
|
277
|
+
rule: rule
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Plugin
|
|
5
|
+
class Activerecord < Rigor::Plugin::Base
|
|
6
|
+
# Tiny inflection helper for the common `ClassName → snake_case_plural`
|
|
7
|
+
# mapping Rails uses to derive table names. Deliberately
|
|
8
|
+
# narrow — handles the regular cases (`User → users`,
|
|
9
|
+
# `BlogPost → blog_posts`, `Category → categories`,
|
|
10
|
+
# `Bus → buses`, `Wolf → wolves`). Irregular plurals
|
|
11
|
+
# (`Person → people`, `Mouse → mice`, `Datum → data`) are
|
|
12
|
+
# NOT handled; the user is expected to declare
|
|
13
|
+
# `self.table_name = "people"` for those.
|
|
14
|
+
#
|
|
15
|
+
# Avoids an `activesupport` runtime dependency. Rails apps
|
|
16
|
+
# that need richer inflection should set explicit table
|
|
17
|
+
# names on the affected models.
|
|
18
|
+
module Inflector
|
|
19
|
+
IRREGULAR_PLURALS = {
|
|
20
|
+
# Common ones we still want to handle without bringing
|
|
21
|
+
# in a full inflection table. Users get the configured
|
|
22
|
+
# explicit table_name route for anything else.
|
|
23
|
+
"person" => "people",
|
|
24
|
+
"child" => "children",
|
|
25
|
+
"datum" => "data"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# `BlogPost` → `blog_posts`. `User::Profile` → `user_profiles`
|
|
31
|
+
# (Rails-style namespacing flattens with underscore).
|
|
32
|
+
def tableize(class_name)
|
|
33
|
+
underscore = underscore(class_name.to_s.gsub("::", "/"))
|
|
34
|
+
# `user/profiles` → `user_profiles`
|
|
35
|
+
underscore = underscore.tr("/", "_")
|
|
36
|
+
pluralize(underscore)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# `BlogPost` → `blog_post`. Standard Rails-style underscore.
|
|
40
|
+
def underscore(camel_case_word)
|
|
41
|
+
word = camel_case_word.to_s.dup
|
|
42
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
43
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
|
44
|
+
word.tr!("-", "_")
|
|
45
|
+
word.downcase!
|
|
46
|
+
word
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `users` → `User`, `blog_posts` → `BlogPost`.
|
|
50
|
+
# Used by the association detector to map an
|
|
51
|
+
# association NAME (`has_many :posts` → `Post`) to the
|
|
52
|
+
# target class without requiring an explicit
|
|
53
|
+
# `class_name:` option. Singularises the word first,
|
|
54
|
+
# then camel-cases. Recognises the same irregular
|
|
55
|
+
# plurals as {.pluralize}.
|
|
56
|
+
def classify(word)
|
|
57
|
+
camelize(singularize(word.to_s))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `posts` → `post`, `categories` → `category`,
|
|
61
|
+
# `wolves` → `wolf`, `buses` → `bus`. The inverse of
|
|
62
|
+
# {.pluralize} for the regular cases this module
|
|
63
|
+
# recognises. Irregular forms (`people` → `person`)
|
|
64
|
+
# round-trip via {IRREGULAR_PLURALS}.
|
|
65
|
+
def singularize(word)
|
|
66
|
+
IRREGULAR_PLURALS.each { |singular, plural| return singular if word == plural }
|
|
67
|
+
|
|
68
|
+
case word
|
|
69
|
+
when /(.*[bcdfghjklmnpqrstvwxz])ies\z/
|
|
70
|
+
"#{Regexp.last_match(1)}y"
|
|
71
|
+
when /(.*[sxz]|.*[cs]h)es\z/
|
|
72
|
+
Regexp.last_match(0)[0..-3]
|
|
73
|
+
when /(.*)ves\z/
|
|
74
|
+
"#{Regexp.last_match(1)}f"
|
|
75
|
+
when /(.+)s\z/
|
|
76
|
+
Regexp.last_match(1)
|
|
77
|
+
else
|
|
78
|
+
word
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `blog_post` → `BlogPost`. Camelizes around `_` and
|
|
83
|
+
# `/` separators; the latter promotes namespace boundaries
|
|
84
|
+
# to `::` (Rails-style).
|
|
85
|
+
def camelize(snake)
|
|
86
|
+
snake.to_s.split("/").map do |segment|
|
|
87
|
+
segment.split("_").map { |part| part.empty? ? part : part[0].upcase + part[1..] }.join
|
|
88
|
+
end.join("::")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# `user` → `users`, `category` → `categories`,
|
|
92
|
+
# `bus` → `buses`, `wolf` → `wolves`. Falls back to a
|
|
93
|
+
# plain `+ "s"` for unrecognised endings.
|
|
94
|
+
def pluralize(word)
|
|
95
|
+
return IRREGULAR_PLURALS[word] if IRREGULAR_PLURALS.key?(word)
|
|
96
|
+
|
|
97
|
+
case word
|
|
98
|
+
when /(.*[bcdfghjklmnpqrstvwxz])y\z/
|
|
99
|
+
# `category` → `categories`, `cherry` → `cherries`
|
|
100
|
+
"#{Regexp.last_match(1)}ies"
|
|
101
|
+
when /(.*[sxz]|.*[cs]h)\z/
|
|
102
|
+
# `bus` → `buses`, `box` → `boxes`, `dish` → `dishes`
|
|
103
|
+
"#{Regexp.last_match(0)}es"
|
|
104
|
+
when /(.*)fe?\z/
|
|
105
|
+
# `wolf` → `wolves`, `knife` → `knives`
|
|
106
|
+
"#{Regexp.last_match(1)}ves"
|
|
107
|
+
else
|
|
108
|
+
"#{word}s"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|