rigortype 0.1.15 → 0.1.16
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 +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +25 -1
- data/lib/rigor/analysis/diagnostic.rb +40 -0
- data/lib/rigor/analysis/runner.rb +61 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +135 -5
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +127 -8
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +52 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
data/sig/rigor.rbs
CHANGED
|
@@ -53,6 +53,11 @@ module Rigor
|
|
|
53
53
|
def error?: () -> bool
|
|
54
54
|
def to_h: () -> Hash[String, untyped]
|
|
55
55
|
def to_s: () -> String
|
|
56
|
+
|
|
57
|
+
# Singleton factories plugins build diagnostics through (the
|
|
58
|
+
# `node` / `location` are Prism values, received as `untyped`).
|
|
59
|
+
def self.from_node: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
60
|
+
def self.from_location: (untyped location, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
class Result
|
|
@@ -90,20 +90,24 @@ Do NOT use this skill for:
|
|
|
90
90
|
A Rigor plugin is a Ruby class that subclasses `Rigor::Plugin::Base`,
|
|
91
91
|
declares a `manifest(id:, version:, …)`, and calls
|
|
92
92
|
`Rigor::Plugin.register(self)` at load time. When `.rigor.yml` lists
|
|
93
|
-
the plugin under `plugins:`, Rigor `require`s it and
|
|
94
|
-
|
|
95
|
-
scope
|
|
96
|
-
|
|
97
|
-
returns an array of
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
the plugin under `plugins:`, Rigor `require`s it and runs its
|
|
94
|
+
**node rules**: the plugin declares `node_rule(Prism::CallNode) { |node,
|
|
95
|
+
scope, path, _fc, context| … }`, and the engine — which owns the single
|
|
96
|
+
AST walk per file — hands every matching node to the block along with a
|
|
97
|
+
`scope` it can query for inferred types. The block returns an array of
|
|
98
|
+
`Rigor::Analysis::Diagnostic` (built via the `diagnostic` helper).
|
|
99
|
+
Optionally the plugin also declares `dynamic_return(receivers:)` /
|
|
100
|
+
`type_specifier(methods:)` to *supply* a return type or narrowing facts
|
|
101
|
+
for call sites the core analyzer types as `Dynamic`. (The older
|
|
102
|
+
`#diagnostics_for_file` / `#flow_contribution_for` fat hooks remain as
|
|
103
|
+
deprecated escape valves — see Phase 2.)
|
|
100
104
|
|
|
101
105
|
## Phase outline
|
|
102
106
|
|
|
103
107
|
| Phase | What | Reference |
|
|
104
108
|
| --- | --- | --- |
|
|
105
109
|
| 1 | Package and scaffold — gem vs project-private layout, gemspec / Gemfile, the plugin class skeleton, `.rigor.yml` activation. | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) |
|
|
106
|
-
| 2 |
|
|
110
|
+
| 2 | Node rules — `node_rule` (engine-owned walk), building `Diagnostic`s via `Base#diagnostic`, querying `scope.type_of`, calling the target library directly instead of reimplementing it (ADR-39: `Plugin::Inflector`, `Base.suggest`), optional `dynamic_return` / `type_specifier`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
|
|
107
111
|
| 3 | Test and ship — fixture-based tests (RSpec / Minitest, no rigor internals), version pinning, README, publish or keep private. | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) |
|
|
108
112
|
|
|
109
113
|
## Reading order — modules
|
|
@@ -111,5 +115,5 @@ return type for call sites the core analyzer types as `Dynamic`.
|
|
|
111
115
|
| Module | Read | Covers |
|
|
112
116
|
| --- | --- | --- |
|
|
113
117
|
| 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
|
|
114
|
-
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `
|
|
118
|
+
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (and the deprecated `flow_contribution_for` escape valve), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
|
|
115
119
|
| 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
|
|
@@ -184,10 +184,11 @@ module Rigor
|
|
|
184
184
|
@module_name = config.fetch("module_name", "Default")
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
-
#
|
|
188
|
-
# `scope` answers inferred-type queries
|
|
189
|
-
# Rigor::Analysis::Diagnostic
|
|
190
|
-
|
|
187
|
+
# The engine owns the AST walk and hands every matching node to
|
|
188
|
+
# this rule. `scope` answers inferred-type queries; return an
|
|
189
|
+
# Array of Rigor::Analysis::Diagnostic (built via `diagnostic`).
|
|
190
|
+
# See 02-walker-and-types.md.
|
|
191
|
+
node_rule Prism::CallNode do |node, scope, path, _file_context, _context|
|
|
191
192
|
[]
|
|
192
193
|
end
|
|
193
194
|
end
|
|
@@ -227,7 +228,7 @@ inert.
|
|
|
227
228
|
|
|
228
229
|
## Output of this module
|
|
229
230
|
|
|
230
|
-
A scaffolded plugin that loads (even if `
|
|
231
|
+
A scaffolded plugin that loads (even if its `node_rule` still
|
|
231
232
|
returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
|
|
232
233
|
([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
|
|
233
234
|
actually analyse.
|
|
@@ -1,57 +1,94 @@
|
|
|
1
|
-
# 02 —
|
|
1
|
+
# 02 — Node rules and types
|
|
2
2
|
|
|
3
|
-
Covers **Phase 2** —
|
|
4
|
-
|
|
3
|
+
Covers **Phase 2** — recognising the DSL's call shapes, emitting
|
|
4
|
+
diagnostics, and (optionally) contributing return types. The engine
|
|
5
|
+
owns the AST walk; you write the per-node check.
|
|
5
6
|
|
|
6
|
-
## `
|
|
7
|
+
## `node_rule` — the core hook (ADR-37)
|
|
8
|
+
|
|
9
|
+
A diagnostic-emitting plugin declares a **node rule**. The engine walks
|
|
10
|
+
each analysed file's AST **once** and hands every node of the declared
|
|
11
|
+
type to your block — so you write the check and never the traversal:
|
|
7
12
|
|
|
8
13
|
```ruby
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
class MyPlugin < Rigor::Plugin::Base
|
|
15
|
+
manifest(id: "my-plugin", version: "0.1.0")
|
|
16
|
+
|
|
17
|
+
node_rule Prism::CallNode do |node, scope, path, file_context, context|
|
|
18
|
+
# node — the Prism::CallNode the engine matched.
|
|
19
|
+
# scope — answers inferred-type queries: scope.type_of(node).
|
|
20
|
+
# path — the file path, for the diagnostic location.
|
|
21
|
+
# file_context — the value node_file_context built (see below); nil if none.
|
|
22
|
+
# context — Rigor::Plugin::NodeContext: lexical ancestors.
|
|
23
|
+
# → return Array<Rigor::Analysis::Diagnostic> ([] to fire nothing)
|
|
24
|
+
end
|
|
14
25
|
end
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
the
|
|
19
|
-
|
|
20
|
-
`
|
|
28
|
+
`node_rule(node_type)` matches by `node.is_a?(node_type)`, so declare
|
|
29
|
+
the concrete class you care about (`Prism::CallNode` for almost every
|
|
30
|
+
DSL plugin) or `Prism::Node` to see everything. The block runs through
|
|
31
|
+
`instance_exec`, so `config`, `services`, `services.fact_store`, and the
|
|
32
|
+
`diagnostic` helper (below) are all in scope. Declare multiple rules
|
|
33
|
+
(they run in declaration order); a plugin that declares none pays zero
|
|
34
|
+
cost. Keep the matching logic in a separate `Analyzer` class once it
|
|
35
|
+
grows — have it take the call node and return location-free `Violation`
|
|
36
|
+
rows the rule positions with `diagnostic` (the bundled plugins follow
|
|
37
|
+
this `Analyzer.violations_for` split, which also makes the logic
|
|
38
|
+
unit-testable without running the whole engine).
|
|
39
|
+
|
|
40
|
+
> The legacy `diagnostics_for_file(path:, scope:, root:)` hook — where
|
|
41
|
+
> you hand-rolled a `def walk` / `compact_child_nodes.each` recursion
|
|
42
|
+
> over `root` yourself — is the **deprecated whole-file escape valve**.
|
|
43
|
+
> Use it only for a genuinely file-scoped result (a single load-error
|
|
44
|
+
> row, or a check that needs the whole parsed file at once). New
|
|
45
|
+
> node-scoped checks use `node_rule`.
|
|
21
46
|
|
|
22
47
|
### Recognising call sites
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
49
|
+
Inside the rule, match on `node.name` (the method name, a Symbol),
|
|
50
|
+
`node.receiver` (nil for an implicit-self call), and
|
|
51
|
+
`node.arguments&.arguments`. For literal `:sym` / `"str"` arguments
|
|
52
|
+
reach for `Rigor::Source::Literals` (`symbol_or_string(node)` /
|
|
53
|
+
`symbol_arguments(call)` / `symbol_arg(call, i)`) rather than
|
|
54
|
+
re-deriving the `unescaped.to_sym` shape.
|
|
55
|
+
|
|
56
|
+
### Two-pass and lexical context
|
|
57
|
+
|
|
58
|
+
- **Same-file collect-then-validate** (gather declared names, then
|
|
59
|
+
validate references): declare `node_file_context { |root, scope| … }`.
|
|
60
|
+
It runs once per file before the walk and its return value is threaded
|
|
61
|
+
to every rule as `file_context`. (A *cross-file* collect belongs in
|
|
62
|
+
`#prepare` + `services.fact_store` instead — see
|
|
63
|
+
[`01-plan-and-scaffold.md`](01-plan-and-scaffold.md).)
|
|
64
|
+
- **Where the node sits**: `context` (a `Rigor::Plugin::NodeContext`)
|
|
65
|
+
carries the lexical ancestor chain — `context.enclosing_def`,
|
|
66
|
+
`context.enclosing_module`, `context.enclosing_block(:describe)`, and
|
|
67
|
+
the raw `context.ancestors`. Read it when the check depends on the
|
|
68
|
+
enclosing class / method / block DSL (the enclosing controller a
|
|
69
|
+
`before_action` sits in, the `describe <Model>` a matcher is under).
|
|
35
70
|
|
|
36
71
|
## Building a `Diagnostic`
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
Use the `Base#diagnostic` helper — it internalises the load-bearing
|
|
74
|
+
1-based `line` / `start_column + 1` convention so you never unpack
|
|
75
|
+
`node.location` by hand:
|
|
40
76
|
|
|
41
77
|
```ruby
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
line: loc.start_line,
|
|
47
|
-
column: loc.start_column + 1, # 1-based column
|
|
48
|
-
message: message,
|
|
49
|
-
severity: severity, # :error | :warning | :info
|
|
50
|
-
rule: rule # short kebab-case id
|
|
51
|
-
)
|
|
78
|
+
node_rule Prism::CallNode do |node, scope, path, _fc, _ctx|
|
|
79
|
+
next [] unless offending?(node)
|
|
80
|
+
|
|
81
|
+
[diagnostic(node, path: path, message: "...", severity: :error, rule: "my-rule")]
|
|
52
82
|
end
|
|
53
83
|
```
|
|
54
84
|
|
|
85
|
+
Pass `location:` (a Prism location) to point at a sub-location rather
|
|
86
|
+
than the whole node — typically `location: node.message_loc`, so a
|
|
87
|
+
method-name diagnostic points at the name, not the receiver-spanning
|
|
88
|
+
whole call. Do **not** set `source_family` — the runner stamps
|
|
89
|
+
`plugin.<manifest.id>` on every returned diagnostic, so anything you set
|
|
90
|
+
is overwritten.
|
|
91
|
+
|
|
55
92
|
`rule` is a short identifier (`dimension-mismatch`,
|
|
56
93
|
`unknown-state`). Rigor namespaces it under your plugin —
|
|
57
94
|
diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
|
|
@@ -87,46 +124,80 @@ Match with `case`/`when` on the carrier class. Treat any carrier you
|
|
|
87
124
|
do not recognise as "decline to act" — never crash on an unexpected
|
|
88
125
|
type.
|
|
89
126
|
|
|
90
|
-
##
|
|
127
|
+
## Use the target library, don't reimplement it (ADR-39)
|
|
128
|
+
|
|
129
|
+
A plugin may **call the pure methods of the library it targets
|
|
130
|
+
directly** instead of reimplementing them — the Ruby analogue of a
|
|
131
|
+
PHPStan extension calling into the real framework. Reimplementing a
|
|
132
|
+
library's rules (an inflector, a status-code table, …) risks diverging
|
|
133
|
+
from the real behaviour, and a wrong *derived* fact (a wrong class /
|
|
134
|
+
helper / column name) is a false positive on working code. So declare
|
|
135
|
+
the target gem as your plugin's dependency and call its safe methods.
|
|
91
136
|
|
|
92
|
-
|
|
137
|
+
The bundled `Rigor::Plugin::Inflector` is the worked example — it calls
|
|
138
|
+
the real `ActiveSupport::Inflector` rather than a hand-rolled
|
|
139
|
+
approximation. If your plugin needs inflection, use it:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
Rigor::Plugin::Inflector.classify("users") # => "User"
|
|
143
|
+
Rigor::Plugin::Inflector.tableize("BlogPost") # => "blog_posts"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For your own target library, follow the same harness: a **fixed
|
|
147
|
+
allow-list** of pure methods, inputs derived from source, and **decline
|
|
148
|
+
(return nil / emit nothing) when the library is unavailable — never
|
|
149
|
+
approximate**. How the call is isolated from Rigor (in-process, a forked
|
|
150
|
+
worker, or a `Ruby::Box`) is a configurable strategy the user picks
|
|
151
|
+
(`plugins_isolation:`); you just call the method.
|
|
152
|
+
|
|
153
|
+
For the common "did you mean …?" suggestion, use the shared helper
|
|
154
|
+
rather than hand-rolling Levenshtein:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
Rigor::Plugin::Base.suggest(typo, known_names) # nearest match, or nil
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Optional — contribute a return type with `dynamic_return` / `type_specifier`
|
|
161
|
+
|
|
162
|
+
> **Critical — these hooks do NOT make a method "defined", so they do
|
|
93
163
|
> NOT suppress `call.undefined-method`.** Method *existence* and call
|
|
94
|
-
> *type* are two independent checks.
|
|
164
|
+
> *type* are two independent checks. A return-type contribution sharpens
|
|
95
165
|
> the type of a call the analyzer has **already resolved to a real
|
|
96
166
|
> method** (turning a `Dynamic` return into something precise). It is
|
|
97
167
|
> never consulted for a receiver/method the analyzer cannot find — that
|
|
98
|
-
> fires `call.undefined-method` first, and a
|
|
99
|
-
>
|
|
100
|
-
>
|
|
101
|
-
>
|
|
102
|
-
>
|
|
103
|
-
>
|
|
104
|
-
>
|
|
105
|
-
>
|
|
168
|
+
> fires `call.undefined-method` first, and a contribution does nothing
|
|
169
|
+
> to silence it. **If your goal is to kill a `call.undefined-method`
|
|
170
|
+
> cluster on a DSL-generated method (the common reason
|
|
171
|
+
> `rigor-project-init` hands off to this skill), the fix is to make the
|
|
172
|
+
> method *exist* in Rigor's view — ship RBS declaring it (see "Shipping
|
|
173
|
+
> RBS for the DSL" below), not a return-type contribution.** Reach for
|
|
174
|
+
> these only when the call already resolves and you want a *better
|
|
175
|
+
> return type*.
|
|
106
176
|
|
|
107
177
|
A plugin can do more than emit diagnostics: it can *supply* the
|
|
108
|
-
inferred return type for a call site the core
|
|
109
|
-
otherwise type as `Dynamic`.
|
|
178
|
+
inferred return type (or narrowing facts) for a call site the core
|
|
179
|
+
analyzer would otherwise type as `Dynamic`. ADR-37 gives two narrow,
|
|
180
|
+
declaratively-gated DSLs — prefer them; the engine indexes plugins by
|
|
181
|
+
their gate so it only calls the block for matching calls:
|
|
110
182
|
|
|
111
183
|
```ruby
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Rigor::
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
184
|
+
# Per-call-site RETURN TYPE, gated on the receiver's class.
|
|
185
|
+
# The block fires only when the receiver type's class is (or inherits
|
|
186
|
+
# from) a declared `receivers:` entry. Return a Rigor::Type or nil.
|
|
187
|
+
dynamic_return receivers: ["Money"] do |call_node, scope|
|
|
188
|
+
next nil unless call_node.name == :+
|
|
189
|
+
Rigor::Type::Combinator.nominal_of("Money")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Post-return NARROWING FACTS, gated on the call's method name.
|
|
193
|
+
# Return an Array of facts (or nil). Used for assertion / predicate
|
|
194
|
+
# narrowing (`assert_kind_of(Foo, x)` ⇒ x is Foo afterwards).
|
|
195
|
+
type_specifier methods: [:assert_kind_of] do |call_node, scope|
|
|
196
|
+
# ... build and return the post-return facts ...
|
|
126
197
|
end
|
|
127
198
|
```
|
|
128
199
|
|
|
129
|
-
Build
|
|
200
|
+
Build return types with `Rigor::Type::Combinator`:
|
|
130
201
|
|
|
131
202
|
```ruby
|
|
132
203
|
Rigor::Type::Combinator.nominal_of("Money") # a class type
|
|
@@ -134,14 +205,27 @@ Rigor::Type::Combinator.constant_of(true) # a literal
|
|
|
134
205
|
Rigor::Type::Combinator.union(a, b) # a union
|
|
135
206
|
```
|
|
136
207
|
|
|
137
|
-
Returning `nil` is always safe — it means "no contribution",
|
|
138
|
-
core analyzer keeps its own answer. Contribute
|
|
208
|
+
Returning `nil` (or `[]`) is always safe — it means "no contribution",
|
|
209
|
+
and the core analyzer keeps its own answer. Contribute only when the
|
|
139
210
|
plugin is confident; a wrong contribution propagates downstream.
|
|
140
211
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
`receivers:` / `methods:` are the greppable gates the
|
|
213
|
+
`rigor plugins --capabilities` catalogue enumerates — run it to see
|
|
214
|
+
exactly what each loaded plugin contributes.
|
|
215
|
+
|
|
216
|
+
> **The deprecated escape valve.** The original fat hook,
|
|
217
|
+
> `flow_contribution_for(call_node:, scope:)`, returns a single
|
|
218
|
+
> `Rigor::FlowContribution` and is still consulted alongside the narrow
|
|
219
|
+
> DSLs. It is retained only for the two shapes the narrow DSLs do not
|
|
220
|
+
> express: a **method-gated return type** (an RSpec `let(:x) { … }`
|
|
221
|
+
> binding, a Sorbet `sig`-driven return — keyed on the method, not a
|
|
222
|
+
> fixed receiver class) and a **dynamic per-project receiver set**
|
|
223
|
+
> (ActiveStorage's `Attached::One` on discovered model classes). If your
|
|
224
|
+
> plugin needs one of those, use `flow_contribution_for`; otherwise
|
|
225
|
+
> prefer `dynamic_return` / `type_specifier`. These return-type surfaces
|
|
226
|
+
> are the most contract-sensitive part of the API — implement one only
|
|
227
|
+
> if the plugin genuinely needs to sharpen call-site types; a
|
|
228
|
+
> diagnostics-only plugin skips them entirely.
|
|
145
229
|
|
|
146
230
|
## Shipping RBS for the DSL — the way to suppress `call.undefined-method`
|
|
147
231
|
|
|
@@ -151,7 +235,7 @@ If the DSL introduces methods or classes that Rigor cannot see (a
|
|
|
151
235
|
Rigor RBS declaring them so *core* inference — not just your plugin —
|
|
152
236
|
treats them as **defined**. This is what removes the
|
|
153
237
|
`call.undefined-method` diagnostics on those methods; nothing else
|
|
154
|
-
(not `
|
|
238
|
+
(not a `node_rule`, not `dynamic_return` / `type_specifier`) makes a
|
|
155
239
|
method exist in Rigor's view.
|
|
156
240
|
|
|
157
241
|
Two ways to wire the RBS, depending on how the plugin is packaged:
|
|
@@ -197,8 +281,8 @@ They compose — many plugins ship both.
|
|
|
197
281
|
|
|
198
282
|
## Output of this module
|
|
199
283
|
|
|
200
|
-
A plugin whose `
|
|
201
|
-
|
|
202
|
-
`
|
|
203
|
-
|
|
284
|
+
A plugin whose `node_rule`(s) recognise the DSL and emit diagnostics
|
|
285
|
+
with correct severities and rule ids — optionally a `dynamic_return` /
|
|
286
|
+
`type_specifier` and a `sig/` bundle. Verify by eye with `rigor check`;
|
|
287
|
+
lock it down with tests in Phase 3
|
|
204
288
|
([`03-test-and-ship.md`](03-test-and-ship.md)).
|
|
@@ -114,9 +114,9 @@ SKILL.md). Concretely:
|
|
|
114
114
|
- Your plugin's own version is normal semver, independent of
|
|
115
115
|
`rigortype`'s.
|
|
116
116
|
- When you bump the `rigortype` pin to a new minor, **re-run the
|
|
117
|
-
full test suite** — the
|
|
118
|
-
type-carrier shapes may have changed. The
|
|
119
|
-
what catch a contract drift.
|
|
117
|
+
full test suite** — the `node_rule` / contribution hook signatures
|
|
118
|
+
or the `Diagnostic` / type-carrier shapes may have changed. The
|
|
119
|
+
fixture CLI tests are what catch a contract drift.
|
|
120
120
|
- State the supported `rigortype` range in the README so users do
|
|
121
121
|
not pair the plugin with an incompatible Rigor.
|
|
122
122
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -69,6 +69,46 @@ dependencies:
|
|
|
69
69
|
- - "<"
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
71
|
version: '5.0'
|
|
72
|
+
- !ruby/object:Gem::Dependency
|
|
73
|
+
name: activesupport
|
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '7.0'
|
|
79
|
+
- - "<"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '9.0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '7.0'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '9.0'
|
|
92
|
+
- !ruby/object:Gem::Dependency
|
|
93
|
+
name: rack
|
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '2.0'
|
|
99
|
+
- - "<"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '4.0'
|
|
102
|
+
type: :development
|
|
103
|
+
prerelease: false
|
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '2.0'
|
|
109
|
+
- - "<"
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '4.0'
|
|
72
112
|
- !ruby/object:Gem::Dependency
|
|
73
113
|
name: parallel_tests
|
|
74
114
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -326,6 +366,7 @@ files:
|
|
|
326
366
|
- lib/rigor/flow_contribution/merger.rb
|
|
327
367
|
- lib/rigor/inference/acceptance.rb
|
|
328
368
|
- lib/rigor/inference/block_parameter_binder.rb
|
|
369
|
+
- lib/rigor/inference/budget_trace.rb
|
|
329
370
|
- lib/rigor/inference/builtins/array_catalog.rb
|
|
330
371
|
- lib/rigor/inference/builtins/comparable_catalog.rb
|
|
331
372
|
- lib/rigor/inference/builtins/complex_catalog.rb
|
|
@@ -411,18 +452,24 @@ files:
|
|
|
411
452
|
- lib/rigor/mcp/server.rb
|
|
412
453
|
- lib/rigor/plugin.rb
|
|
413
454
|
- lib/rigor/plugin/access_denied_error.rb
|
|
455
|
+
- lib/rigor/plugin/additional_initializer.rb
|
|
414
456
|
- lib/rigor/plugin/base.rb
|
|
415
457
|
- lib/rigor/plugin/blueprint.rb
|
|
458
|
+
- lib/rigor/plugin/box.rb
|
|
416
459
|
- lib/rigor/plugin/fact_store.rb
|
|
460
|
+
- lib/rigor/plugin/inflector.rb
|
|
417
461
|
- lib/rigor/plugin/io_boundary.rb
|
|
462
|
+
- lib/rigor/plugin/isolation.rb
|
|
418
463
|
- lib/rigor/plugin/load_error.rb
|
|
419
464
|
- lib/rigor/plugin/loader.rb
|
|
420
465
|
- lib/rigor/plugin/macro.rb
|
|
421
466
|
- lib/rigor/plugin/macro/block_as_method.rb
|
|
422
467
|
- lib/rigor/plugin/macro/external_file.rb
|
|
423
468
|
- lib/rigor/plugin/macro/heredoc_template.rb
|
|
469
|
+
- lib/rigor/plugin/macro/nested_class_template.rb
|
|
424
470
|
- lib/rigor/plugin/macro/trait_registry.rb
|
|
425
471
|
- lib/rigor/plugin/manifest.rb
|
|
472
|
+
- lib/rigor/plugin/node_context.rb
|
|
426
473
|
- lib/rigor/plugin/protocol_contract.rb
|
|
427
474
|
- lib/rigor/plugin/registry.rb
|
|
428
475
|
- lib/rigor/plugin/services.rb
|
|
@@ -447,6 +494,7 @@ files:
|
|
|
447
494
|
- lib/rigor/sig_gen/write_result.rb
|
|
448
495
|
- lib/rigor/sig_gen/writer.rb
|
|
449
496
|
- lib/rigor/source.rb
|
|
497
|
+
- lib/rigor/source/literals.rb
|
|
450
498
|
- lib/rigor/source/node_locator.rb
|
|
451
499
|
- lib/rigor/source/node_walker.rb
|
|
452
500
|
- lib/rigor/testing.rb
|
|
@@ -506,7 +554,6 @@ files:
|
|
|
506
554
|
- plugins/rigor-activerecord/lib/rigor-activerecord.rb
|
|
507
555
|
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb
|
|
508
556
|
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb
|
|
509
|
-
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb
|
|
510
557
|
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb
|
|
511
558
|
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb
|
|
512
559
|
- plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb
|
|
@@ -546,6 +593,8 @@ files:
|
|
|
546
593
|
- plugins/rigor-hanami/lib/rigor/plugin/hanami.rb
|
|
547
594
|
- plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb
|
|
548
595
|
- plugins/rigor-hanami/sig/hanami_action.rbs
|
|
596
|
+
- plugins/rigor-mangrove/lib/rigor-mangrove.rb
|
|
597
|
+
- plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb
|
|
549
598
|
- plugins/rigor-minitest/lib/rigor-minitest.rb
|
|
550
599
|
- plugins/rigor-minitest/lib/rigor/plugin/minitest.rb
|
|
551
600
|
- plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb
|
|
@@ -680,6 +729,7 @@ require_paths:
|
|
|
680
729
|
- plugins/rigor-factorybot/lib
|
|
681
730
|
- plugins/rigor-graphql/lib
|
|
682
731
|
- plugins/rigor-hanami/lib
|
|
732
|
+
- plugins/rigor-mangrove/lib
|
|
683
733
|
- plugins/rigor-minitest/lib
|
|
684
734
|
- plugins/rigor-pundit/lib
|
|
685
735
|
- plugins/rigor-rails/lib
|
|
@@ -1,114 +0,0 @@
|
|
|
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
|