rigortype 0.1.12 → 0.1.13
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/check_rules.rb +96 -3
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/scope.rb +14 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +1 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +14 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# 02 — Act on the classification, then refresh
|
|
2
|
+
|
|
3
|
+
Covers **Phase 3** (act per site) and **Phase 4** (refresh the
|
|
4
|
+
baseline). Input: the per-site classifications from
|
|
5
|
+
[`01-classify.md`](01-classify.md).
|
|
6
|
+
|
|
7
|
+
## Phase 3 — Act
|
|
8
|
+
|
|
9
|
+
### Real bug → fix the code
|
|
10
|
+
|
|
11
|
+
Propose the fix and offer to apply it. The fix is ordinary code work
|
|
12
|
+
— the value is that Rigor surfaced a defect that was latent because
|
|
13
|
+
the line is rarely exercised. Common shapes:
|
|
14
|
+
|
|
15
|
+
- `possible-nil-receiver` → add the missing guard (`return unless x`,
|
|
16
|
+
`x&.method`, an early raise), or fix the upstream code so the value
|
|
17
|
+
is never `nil` here.
|
|
18
|
+
- `undefined-method` typo → correct the method name.
|
|
19
|
+
- argument-type mismatch → pass the right type, or fix the signature.
|
|
20
|
+
|
|
21
|
+
After a real-bug fix the diagnostic is *gone*, not suppressed — the
|
|
22
|
+
bucket count drops on its own.
|
|
23
|
+
|
|
24
|
+
### Stylistic / safe → `# rigor:disable` with intent
|
|
25
|
+
|
|
26
|
+
The code is staying as it is; the suppression is a deliberate,
|
|
27
|
+
recorded decision. Place a per-line comment at the end of the
|
|
28
|
+
offending line:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
config.fetch(:timeout) # rigor:disable call.possible-nil-receiver — set in initializer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Placement rules:
|
|
35
|
+
|
|
36
|
+
- **Per-line `# rigor:disable <rule>`** is the default. It keeps the
|
|
37
|
+
suppression visible exactly where it applies, and a future reader
|
|
38
|
+
sees the intent. Always name the specific rule, never `all`.
|
|
39
|
+
- Add a short reason after the rule — *why* the site is safe. A bare
|
|
40
|
+
`# rigor:disable` rots; `# rigor:disable … — set in initializer`
|
|
41
|
+
survives review.
|
|
42
|
+
- **Per-file `# rigor:disable-file <rule>`** only when one file has
|
|
43
|
+
many sites of the same rule and they are all the same safe idiom.
|
|
44
|
+
Escalate this to the user as a decision (it is coarser — it also
|
|
45
|
+
silences *future* sites of that rule in the file).
|
|
46
|
+
|
|
47
|
+
A `# rigor:disable`d diagnostic is suppressed *before* the baseline
|
|
48
|
+
filter, so once the comment is in place the site no longer counts
|
|
49
|
+
toward its bucket.
|
|
50
|
+
|
|
51
|
+
### False positive → open a Rigor issue
|
|
52
|
+
|
|
53
|
+
Leave the site baselined (do not `# rigor:disable` it — that would
|
|
54
|
+
imply the code is the thing to live with, when actually Rigor is).
|
|
55
|
+
Open an issue on the Rigor project:
|
|
56
|
+
|
|
57
|
+
<https://github.com/rigortype/rigor/issues>
|
|
58
|
+
|
|
59
|
+
A useful issue includes:
|
|
60
|
+
|
|
61
|
+
- The rule id and the exact diagnostic message.
|
|
62
|
+
- A **minimal** code snippet that reproduces the false positive —
|
|
63
|
+
reduce it to the smallest shape that still mis-fires.
|
|
64
|
+
- What the correct inference should be, and why.
|
|
65
|
+
- The `rigor version` output.
|
|
66
|
+
|
|
67
|
+
This is the feedback loop that makes the analyzer better — a false
|
|
68
|
+
positive reported with a minimal repro is far more actionable than
|
|
69
|
+
one buried in a baseline. While the issue is open the baseline keeps
|
|
70
|
+
the site quiet.
|
|
71
|
+
|
|
72
|
+
## Phase 4 — Refresh the baseline
|
|
73
|
+
|
|
74
|
+
After working a rule (fixes applied, `# rigor:disable` comments
|
|
75
|
+
added, issues filed), the live diagnostic count for the touched
|
|
76
|
+
buckets has dropped below the recorded count. Make the baseline
|
|
77
|
+
reflect reality.
|
|
78
|
+
|
|
79
|
+
First, inspect the drift:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
rigor baseline drift
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This reports each bucket as `within` / `over` / `cleared` /
|
|
86
|
+
`reducible`. After a reduction session you expect `cleared` (the
|
|
87
|
+
bucket is now empty) and `reducible` (the bucket shrank but is not
|
|
88
|
+
empty) rows.
|
|
89
|
+
|
|
90
|
+
Then refresh:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
rigor baseline regenerate
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`regenerate` rewrites `.rigor-baseline.yml` from a fresh `rigor
|
|
97
|
+
check` run — cleared buckets disappear, reducible buckets get their
|
|
98
|
+
new lower counts. This is the command that *banks* the session's
|
|
99
|
+
gains: until you regenerate, the baseline still records the old,
|
|
100
|
+
higher numbers.
|
|
101
|
+
|
|
102
|
+
`rigor baseline prune` is the narrower tool — it drops only the
|
|
103
|
+
fully-`cleared` buckets and leaves reducible ones at their old count.
|
|
104
|
+
Prefer `regenerate` at the end of a reduction session; it captures
|
|
105
|
+
both kinds of progress in one step.
|
|
106
|
+
|
|
107
|
+
Commit the updated `.rigor-baseline.yml` together with the code
|
|
108
|
+
fixes and `# rigor:disable` comments, so the smaller baseline and the
|
|
109
|
+
work that earned it land together.
|
|
110
|
+
|
|
111
|
+
## Verifying the session
|
|
112
|
+
|
|
113
|
+
Confirm the gains held:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
rigor baseline drift # expect "No drift detected" after regenerate
|
|
117
|
+
rigor check # the project's gate — should still pass
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
If the project's CI uses `rigor check --baseline-strict`, the run
|
|
121
|
+
after `regenerate` should be clean — a stale baseline (numbers not
|
|
122
|
+
refreshed) is exactly the deficit drift that gate fails on.
|
|
123
|
+
|
|
124
|
+
## Output of this module — session complete
|
|
125
|
+
|
|
126
|
+
- Code fixes for the real bugs.
|
|
127
|
+
- Intentional, reasoned `# rigor:disable` comments for the
|
|
128
|
+
stylistic-safe sites.
|
|
129
|
+
- Rigor issues filed for the false positives.
|
|
130
|
+
- A regenerated, smaller `.rigor-baseline.yml`, committed with the
|
|
131
|
+
work.
|
|
132
|
+
|
|
133
|
+
Run the skill again next session to take the next rule.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-plugin-author
|
|
3
|
+
description: |
|
|
4
|
+
Author a Rigor plugin in your own repository — a standalone rigor-prefixed gem or a project-private plugin — to teach Rigor about an application DSL, framework, or metaprogramming pattern. Covers gemspec / Gemfile wiring, the plugin class and AST walker, return-type contributions, fixture-based testing (RSpec or Minitest), and version pinning against the pre-1.0 plugin contract. Triggers: "write a Rigor plugin for our DSL", "extend Rigor for X in this project", "make Rigor understand our macro". NOT for onboarding a project (use rigor-project-init) or reducing a baseline (use rigor-baseline-reduce).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.1.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rigor Plugin Author (external)
|
|
12
|
+
|
|
13
|
+
Author a Rigor plugin **in your own repository** — to teach Rigor a
|
|
14
|
+
DSL, framework convention, or metaprogramming pattern its core
|
|
15
|
+
analyzer cannot follow. The result is either a standalone
|
|
16
|
+
`rigor-<id>` gem or a project-private plugin.
|
|
17
|
+
|
|
18
|
+
This skill targets **external authors**: you depend on the published
|
|
19
|
+
`rigortype` gem (`bundle add rigortype`) and use the public plugin
|
|
20
|
+
API surface — `Rigor::Plugin::Base` and friends. It does not assume
|
|
21
|
+
the rigor monorepo's `Makefile`, `spec/integration/` helpers, or Nix
|
|
22
|
+
environment.
|
|
23
|
+
|
|
24
|
+
> **Authoring inside the rigor monorepo instead?** If you are adding
|
|
25
|
+
> a plugin to rigor's own `plugins/` or `examples/` tree, use the
|
|
26
|
+
> contributor `rigor-plugin-author` skill bundled in that repo — it
|
|
27
|
+
> covers the in-repo layout, `plugin_helpers.rb`, and `make verify`.
|
|
28
|
+
> This skill is for plugins that live in *your* project.
|
|
29
|
+
|
|
30
|
+
## Important — the plugin contract is a preview (pre-1.0)
|
|
31
|
+
|
|
32
|
+
Rigor's plugin contract (ADR-2) is **not yet frozen**. It stabilises
|
|
33
|
+
at `rigortype` v0.2.0. Until then, treat each `rigortype` minor
|
|
34
|
+
release as potentially contract-changing:
|
|
35
|
+
|
|
36
|
+
- **Pin `rigortype` tightly** — `>= 0.1.0, < 0.2.0` in your gemspec
|
|
37
|
+
or Gemfile. Do not float across the v0.2.0 boundary blind.
|
|
38
|
+
- Expect to **revisit your plugin** when you bump `rigortype` to the
|
|
39
|
+
next minor. The walker hook signature, the `Diagnostic` shape, and
|
|
40
|
+
the type carriers may shift.
|
|
41
|
+
- After v0.2.0 the contract is stable and ordinary semver applies.
|
|
42
|
+
|
|
43
|
+
Tell the user this up front. A plugin written today is a preview
|
|
44
|
+
artefact, valuable but not yet on a stable foundation.
|
|
45
|
+
|
|
46
|
+
## Phase 0 — Standalone gem or project-private?
|
|
47
|
+
|
|
48
|
+
Decide where the plugin lives before scaffolding anything.
|
|
49
|
+
|
|
50
|
+
| Signal | Build it as |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| The DSL/library is reusable across projects, or you want to publish it | **Standalone `rigor-<id>` gem** — own repo, own gemspec, RubyGems-publishable |
|
|
53
|
+
| The pattern is specific to *one* application's own code / in-house DSL | **Project-private plugin** — lives inside the app repo, never published |
|
|
54
|
+
|
|
55
|
+
Both produce the same plugin class and walker; they differ only in
|
|
56
|
+
packaging and activation (Phase 1). When unsure, default to
|
|
57
|
+
**project-private** — it is less ceremony, and a plugin can always
|
|
58
|
+
be extracted into a gem later.
|
|
59
|
+
|
|
60
|
+
Do NOT use this skill for:
|
|
61
|
+
|
|
62
|
+
- **Onboarding a project to Rigor** (writing `.rigor.yml`, choosing
|
|
63
|
+
plugins) → `rigor-project-init`.
|
|
64
|
+
- **Reducing a baseline** → `rigor-baseline-reduce`.
|
|
65
|
+
- **Editing an existing, already-working plugin** — that is ordinary
|
|
66
|
+
code work; modify the plugin class directly.
|
|
67
|
+
|
|
68
|
+
## How a plugin works — the one-paragraph model
|
|
69
|
+
|
|
70
|
+
A Rigor plugin is a Ruby class that subclasses `Rigor::Plugin::Base`,
|
|
71
|
+
declares a `manifest(id:, version:, …)`, and calls
|
|
72
|
+
`Rigor::Plugin.register(self)` at load time. When `.rigor.yml` lists
|
|
73
|
+
the plugin under `plugins:`, Rigor `require`s it and, for every
|
|
74
|
+
analysed file, calls the plugin's `#diagnostics_for_file(path:,
|
|
75
|
+
scope:, root:)` — handing it the file's Prism AST (`root`) and a
|
|
76
|
+
`scope` it can query for inferred types. The plugin walks the AST and
|
|
77
|
+
returns an array of `Rigor::Analysis::Diagnostic`. Optionally it also
|
|
78
|
+
implements `#flow_contribution_for(call_node:, scope:)` to *supply* a
|
|
79
|
+
return type for call sites the core analyzer types as `Dynamic`.
|
|
80
|
+
|
|
81
|
+
## Phase outline
|
|
82
|
+
|
|
83
|
+
| Phase | What | Reference |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| 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) |
|
|
86
|
+
| 2 | The walker — `diagnostics_for_file`, building `Diagnostic`s, querying `scope.type_of`, optional `flow_contribution_for`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
|
|
87
|
+
| 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) |
|
|
88
|
+
|
|
89
|
+
## Reading order — modules
|
|
90
|
+
|
|
91
|
+
| Module | Read | Covers |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| 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. |
|
|
94
|
+
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `diagnostics_for_file` AST walk over Prism nodes, the `Diagnostic` constructor shape, asking the analyzer for inferred types via `scope.type_of`, the optional `flow_contribution_for` return-type hook, and shipping `sig/*.rbs` so the DSL's types are visible. |
|
|
95
|
+
| 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. |
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# 01 — Package and scaffold
|
|
2
|
+
|
|
3
|
+
Covers **Phase 1**. Output: a directory tree, a plugin class
|
|
4
|
+
skeleton that registers itself, and an `.rigor.yml` that activates
|
|
5
|
+
it.
|
|
6
|
+
|
|
7
|
+
## Naming
|
|
8
|
+
|
|
9
|
+
- **Plugin id** — kebab-case, starts with a letter, matches
|
|
10
|
+
`/\A[a-z][a-z0-9._-]*\z/`. Descriptive: `units`, `myapp-dsl`,
|
|
11
|
+
`legacy-macros`.
|
|
12
|
+
- **Require name** — Rigor activates a plugin by `require`-ing the
|
|
13
|
+
name in the `.rigor.yml` `plugins:` entry. The convention is
|
|
14
|
+
`rigor-<id>`, and the file that name resolves to must, when
|
|
15
|
+
loaded, call `Rigor::Plugin.register`.
|
|
16
|
+
- **Ruby class** — CamelCase under `Rigor::Plugin`, e.g.
|
|
17
|
+
`Rigor::Plugin::MyappDsl`.
|
|
18
|
+
|
|
19
|
+
## Standalone gem layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
rigor-<id>/ # its own repository
|
|
23
|
+
├── README.md
|
|
24
|
+
├── rigor-<id>.gemspec
|
|
25
|
+
├── Gemfile # gem "rigortype" (dev); gemspec
|
|
26
|
+
├── lib/
|
|
27
|
+
│ ├── rigor-<id>.rb # require name → require_relative the class
|
|
28
|
+
│ └── rigor/plugin/
|
|
29
|
+
│ └── <id>.rb # the plugin class + Rigor::Plugin.register
|
|
30
|
+
│ └── <id>/ # only if it needs helpers
|
|
31
|
+
│ └── analyzer.rb # AST walker extracted out of the class
|
|
32
|
+
├── sig/ # optional — RBS for the DSL (Phase 2)
|
|
33
|
+
└── spec/ or test/ # fixture tests (Phase 3)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Gemspec template
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# rigor-<id>.gemspec
|
|
40
|
+
# frozen_string_literal: true
|
|
41
|
+
|
|
42
|
+
Gem::Specification.new do |spec|
|
|
43
|
+
spec.name = "rigor-<id>"
|
|
44
|
+
spec.version = "0.1.0"
|
|
45
|
+
spec.authors = ["Your Name"]
|
|
46
|
+
spec.summary = "Rigor plugin: <one line>."
|
|
47
|
+
spec.description = "<two sentences naming the DSL / API the plugin types>."
|
|
48
|
+
spec.license = "MIT" # your choice
|
|
49
|
+
spec.required_ruby_version = ">= 3.2"
|
|
50
|
+
|
|
51
|
+
spec.files = Dir.glob(%w[README.md lib/**/*.rb sig/**/*.rbs])
|
|
52
|
+
spec.require_paths = ["lib"]
|
|
53
|
+
|
|
54
|
+
spec.add_dependency "prism", ">= 1.0", "< 2.0"
|
|
55
|
+
# Pin tightly — the plugin contract is pre-1.0 (see SKILL.md).
|
|
56
|
+
spec.add_dependency "rigortype", ">= 0.1.0", "< 0.2.0"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`prism` is Rigor's parser; a plugin walks `Prism::Node`s, so depend
|
|
61
|
+
on it directly.
|
|
62
|
+
|
|
63
|
+
## Project-private layout
|
|
64
|
+
|
|
65
|
+
A project-private plugin lives inside the application repo and is
|
|
66
|
+
never published. Two ways to make `require "rigor-<id>"` succeed:
|
|
67
|
+
|
|
68
|
+
### Recommended — a path gem
|
|
69
|
+
|
|
70
|
+
Keep the plugin in a subdirectory with its own gemspec, and reference
|
|
71
|
+
it from the app's `Gemfile` by path:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
your-app/
|
|
75
|
+
├── Gemfile
|
|
76
|
+
├── rigor-plugin/ # the plugin, unpublished
|
|
77
|
+
│ ├── rigor-myapp.gemspec
|
|
78
|
+
│ └── lib/
|
|
79
|
+
│ ├── rigor-myapp.rb
|
|
80
|
+
│ └── rigor/plugin/myapp.rb
|
|
81
|
+
└── .rigor.yml
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# your-app/Gemfile
|
|
86
|
+
gem "rigortype", "~> 0.1.0"
|
|
87
|
+
gem "rigor-myapp", path: "rigor-plugin"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`bundle install` then puts `rigor-myapp` on the load path, so Rigor's
|
|
91
|
+
`require "rigor-myapp"` resolves. This keeps the plugin versioned,
|
|
92
|
+
testable, and trivially promotable to a real gem later.
|
|
93
|
+
|
|
94
|
+
### Simplest — a bare file on the load path
|
|
95
|
+
|
|
96
|
+
If you do not want a gemspec at all, drop `rigor-myapp.rb` somewhere
|
|
97
|
+
and run `rigor` with that directory on `RUBYLIB`:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
RUBYLIB=rigor-ext rigor check
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Workable, but the `RUBYLIB` has to be set on every invocation (CI
|
|
108
|
+
included). Prefer the path gem unless the plugin is a throwaway.
|
|
109
|
+
|
|
110
|
+
## The plugin class skeleton
|
|
111
|
+
|
|
112
|
+
`lib/rigor-<id>.rb` — the require entry point:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# frozen_string_literal: true
|
|
116
|
+
|
|
117
|
+
require_relative "rigor/plugin/<id>"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`lib/rigor/plugin/<id>.rb` — the plugin class:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# frozen_string_literal: true
|
|
124
|
+
|
|
125
|
+
require "rigor/plugin"
|
|
126
|
+
|
|
127
|
+
module Rigor
|
|
128
|
+
module Plugin
|
|
129
|
+
class MyappDsl < Rigor::Plugin::Base
|
|
130
|
+
manifest(
|
|
131
|
+
id: "<id>",
|
|
132
|
+
version: "0.1.0",
|
|
133
|
+
description: "<one line>",
|
|
134
|
+
# Optional: declare config keys the user may set under
|
|
135
|
+
# `.rigor.yml` plugins: [{ gem:, config: { … } }].
|
|
136
|
+
config_schema: {
|
|
137
|
+
# "module_name" => :string,
|
|
138
|
+
# "rules" => :array,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Called once at load time with the service container.
|
|
143
|
+
# Read config defaults here. `config` is the validated
|
|
144
|
+
# user config Hash.
|
|
145
|
+
def init(_services)
|
|
146
|
+
@module_name = config.fetch("module_name", "Default")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Called per analysed file. `root` is the file's Prism AST,
|
|
150
|
+
# `scope` answers inferred-type queries. Return an Array of
|
|
151
|
+
# Rigor::Analysis::Diagnostic. See 02-walker-and-types.md.
|
|
152
|
+
def diagnostics_for_file(path:, scope:, root:)
|
|
153
|
+
[]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Rigor::Plugin.register(MyappDsl)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`Rigor::Plugin.register` at the bottom is mandatory — the loader
|
|
163
|
+
`require`s the gem, then looks for a freshly-registered plugin
|
|
164
|
+
class. A gem that registers nothing fails to load with a clear
|
|
165
|
+
error.
|
|
166
|
+
|
|
167
|
+
## Activate the plugin in `.rigor.yml`
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
plugins:
|
|
171
|
+
- rigor-<id>
|
|
172
|
+
|
|
173
|
+
# Hash form when the plugin takes config:
|
|
174
|
+
# plugins:
|
|
175
|
+
# - gem: rigor-<id>
|
|
176
|
+
# config:
|
|
177
|
+
# module_name: MyApp
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Confirm activation with the public CLI:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
rigor check
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
A misconfigured plugin surfaces as a `plugin-loader` diagnostic
|
|
187
|
+
rather than crashing the run — read that message if the plugin seems
|
|
188
|
+
inert.
|
|
189
|
+
|
|
190
|
+
## Output of this module
|
|
191
|
+
|
|
192
|
+
A scaffolded plugin that loads (even if `diagnostics_for_file` still
|
|
193
|
+
returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
|
|
194
|
+
([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
|
|
195
|
+
actually analyse.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# 02 — The walker and types
|
|
2
|
+
|
|
3
|
+
Covers **Phase 2** — making `diagnostics_for_file` analyse the AST,
|
|
4
|
+
emit diagnostics, and (optionally) contribute return types.
|
|
5
|
+
|
|
6
|
+
## `diagnostics_for_file` — the core hook
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
def diagnostics_for_file(path:, scope:, root:)
|
|
10
|
+
# root — Prism::Node, the parsed file (a ProgramNode).
|
|
11
|
+
# scope — answers inferred-type queries: scope.type_of(node).
|
|
12
|
+
# path — the file path, for Diagnostic#path.
|
|
13
|
+
# → return Array<Rigor::Analysis::Diagnostic>
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Walk `root` with a Prism visitor or a recursive descent, recognise
|
|
18
|
+
the DSL's call shapes, and collect diagnostics. Keep the walk in a
|
|
19
|
+
separate `Analyzer` class once it grows past a few methods — pass it
|
|
20
|
+
`path` and let it return the diagnostic array.
|
|
21
|
+
|
|
22
|
+
### Recognising call sites
|
|
23
|
+
|
|
24
|
+
Most DSL plugins key off `Prism::CallNode`:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
def each_call(node, &block)
|
|
28
|
+
block.call(node) if node.is_a?(Prism::CallNode)
|
|
29
|
+
node&.compact_child_nodes&.each { |child| each_call(child, &block) }
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then match on `node.name` (the method name, a Symbol),
|
|
34
|
+
`node.receiver`, and `node.arguments&.arguments`.
|
|
35
|
+
|
|
36
|
+
## Building a `Diagnostic`
|
|
37
|
+
|
|
38
|
+
Every diagnostic the plugin emits is a `Rigor::Analysis::Diagnostic`.
|
|
39
|
+
A small constructor helper keeps the call sites clean:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
def diagnostic(path, node, severity:, rule:, message:)
|
|
43
|
+
loc = node.location
|
|
44
|
+
Rigor::Analysis::Diagnostic.new(
|
|
45
|
+
path: path,
|
|
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
|
+
)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`rule` is a short identifier (`dimension-mismatch`,
|
|
56
|
+
`unknown-state`). Rigor namespaces it under your plugin —
|
|
57
|
+
diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
|
|
58
|
+
qualified id is what `.rigor.yml` `disable:` and the baseline key
|
|
59
|
+
on. Pick `severity`:
|
|
60
|
+
|
|
61
|
+
- `:error` — a real defect (a type mismatch, a call that will
|
|
62
|
+
raise). Fails `rigor check`.
|
|
63
|
+
- `:warning` — suspicious but not certainly wrong.
|
|
64
|
+
- `:info` — informational; surfaces inferred facts without
|
|
65
|
+
judgement.
|
|
66
|
+
|
|
67
|
+
## Asking the analyzer for types — `scope.type_of`
|
|
68
|
+
|
|
69
|
+
A plugin does not have to infer types itself. `scope.type_of(node)`
|
|
70
|
+
returns the type the core analyzer inferred for any AST node — the
|
|
71
|
+
plugin can build on it:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
receiver_type = scope.type_of(call_node.receiver)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The returned object is one of the `Rigor::Type::*` carriers. The
|
|
78
|
+
ones a plugin meets most:
|
|
79
|
+
|
|
80
|
+
- `Rigor::Type::Nominal` — a class type; `#class_name` is the
|
|
81
|
+
String.
|
|
82
|
+
- `Rigor::Type::Constant` — a literal value; `#value` is the Ruby
|
|
83
|
+
object.
|
|
84
|
+
- `Rigor::Type::IntegerRange` — a bounded integer.
|
|
85
|
+
|
|
86
|
+
Match with `case`/`when` on the carrier class. Treat any carrier you
|
|
87
|
+
do not recognise as "decline to act" — never crash on an unexpected
|
|
88
|
+
type.
|
|
89
|
+
|
|
90
|
+
## Optional — contribute a return type with `flow_contribution_for`
|
|
91
|
+
|
|
92
|
+
A plugin can do more than emit diagnostics: it can *supply* the
|
|
93
|
+
inferred return type for a call site the core analyzer would
|
|
94
|
+
otherwise type as `Dynamic`. Implement `flow_contribution_for`:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
def flow_contribution_for(call_node:, scope:)
|
|
98
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
99
|
+
# ... decide the call site's real return type ...
|
|
100
|
+
return nil if undecidable # nil = "I have nothing to add"
|
|
101
|
+
|
|
102
|
+
Rigor::FlowContribution.new(
|
|
103
|
+
return_type: a_rigor_type,
|
|
104
|
+
provenance: Rigor::FlowContribution::Provenance.new(
|
|
105
|
+
source_family: "plugin.#{manifest.id}",
|
|
106
|
+
plugin_id: manifest.id,
|
|
107
|
+
node: call_node,
|
|
108
|
+
descriptor: nil
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Build the `return_type` with `Rigor::Type::Combinator`:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
Rigor::Type::Combinator.nominal_of("Money") # a class type
|
|
118
|
+
Rigor::Type::Combinator.constant_of(true) # a literal
|
|
119
|
+
Rigor::Type::Combinator.union(a, b) # a union
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Returning `nil` is always safe — it means "no contribution", and the
|
|
123
|
+
core analyzer keeps its own answer. Contribute a type only when the
|
|
124
|
+
plugin is confident; a wrong contribution propagates downstream.
|
|
125
|
+
|
|
126
|
+
This hook is the most contract-sensitive surface — it is the part
|
|
127
|
+
most likely to shift before v0.2.0. Implement it only if the plugin
|
|
128
|
+
genuinely needs to sharpen call-site types; a diagnostics-only
|
|
129
|
+
plugin can skip it entirely.
|
|
130
|
+
|
|
131
|
+
## Shipping RBS for the DSL
|
|
132
|
+
|
|
133
|
+
If the DSL introduces methods or classes that Rigor cannot see (a
|
|
134
|
+
`Money` class defined by metaprogramming, methods mixed into
|
|
135
|
+
`Numeric`), give Rigor RBS for them so *core* inference — not just
|
|
136
|
+
your plugin — understands them. Ship `sig/*.rbs` with the plugin (or
|
|
137
|
+
in the consuming project) and point `.rigor.yml` at it:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
signature_paths:
|
|
141
|
+
- sig
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
RBS covers what the *shape* of the DSL is; the plugin walker covers
|
|
145
|
+
the *dynamic* parts RBS cannot express (a value computed from a
|
|
146
|
+
literal argument, a dimensional rule). They compose — many plugins
|
|
147
|
+
ship both.
|
|
148
|
+
|
|
149
|
+
## Output of this module
|
|
150
|
+
|
|
151
|
+
A plugin whose `diagnostics_for_file` recognises the DSL and emits
|
|
152
|
+
diagnostics with correct severities and rule ids — optionally a
|
|
153
|
+
`flow_contribution_for` and a `sig/` bundle. Verify by eye with
|
|
154
|
+
`rigor check`; lock it down with tests in Phase 3
|
|
155
|
+
([`03-test-and-ship.md`](03-test-and-ship.md)).
|