rigor-module-graph 0.1.0 → 0.1.2
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/CHANGELOG.md +62 -1
- data/README.md +107 -105
- data/lib/rigor/module_graph/cli.rb +75 -22
- data/lib/rigor/module_graph/status_reporter.rb +94 -0
- data/lib/rigor/module_graph/version.rb +1 -1
- data/lib/rigor-module-graph.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cea3ca68d705ecbe0f3534c5b3374e7e1dfd0f01edb6a0b9e64ed4c775cb166d
|
|
4
|
+
data.tar.gz: 70d09cd49d66f118948f2ec142d4f7a6c068a6ce098cddf97b3dd7d997a507d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fec0afc7142600d308187afb293484818e545e63d5e21c08221412e0bcbca98db5cbf6e1e30c3b45ad46a30459779dcfe816459b9009494c5fbd4d0265a0dea
|
|
7
|
+
data.tar.gz: 1795a811ff1b184945dc55fb6a5451d09f895b85b778d1d98145f5fae2ad5f5aa0822dbafed51fad143ae3c32a8b75a5f3beec55cefdd3e9164658aa6e25e019
|
data/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,65 @@ Categories:
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [0.1.2] — 2026-06-20
|
|
21
|
+
|
|
22
|
+
First release that exercises the full automated pipeline end
|
|
23
|
+
to end — Trusted Publishing + GitHub Release + asset upload
|
|
24
|
+
all drive off a single `gh workflow run release.yml` after the
|
|
25
|
+
tag is pushed.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `view` and `collect` now emit step-level progress on stderr:
|
|
30
|
+
`==> Running rigor check ...`, post-step counts (`18 edge(s),
|
|
31
|
+
16 node(s)`), and inline elapsed time (`done (428ms)`).
|
|
32
|
+
TTY-aware — the start / done halves render inline on a
|
|
33
|
+
terminal, on separate lines for redirected output, so logs
|
|
34
|
+
stay grep-friendly. `-q` / `--quiet` suppresses the progress
|
|
35
|
+
output for scripted use; the final `wrote N edge(s) to ...`
|
|
36
|
+
summary line stays. Driven by a new `StatusReporter` class
|
|
37
|
+
pinned by `test/rigor/module_graph/status_reporter_test.rb`.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- README restructured along the install → getting started →
|
|
42
|
+
usage → configuration flow. The "How it works" walkthrough
|
|
43
|
+
(pipeline diagram + the "not a call graph" framing) moves to
|
|
44
|
+
`docs/how-it-works.md` so the README stays focused on
|
|
45
|
+
"what do I type". Configuration section now notes that
|
|
46
|
+
`.rigor.yml` is required (rigor reads it to discover the
|
|
47
|
+
plugin), with a two-line minimum example up top and the
|
|
48
|
+
fully-elaborated default form below.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- RDoc generation now parses Markdown instead of RDoc syntax,
|
|
53
|
+
so `` images in `README.md` / `CHANGELOG.md` /
|
|
54
|
+
`docs/*.md` actually render. `Rake::Task[:rdoc]` is enhanced
|
|
55
|
+
to copy `examples/billing/graph.svg` (and any future
|
|
56
|
+
`RDOC_ASSET_PATHS` entries) into `doc/` so the generated site
|
|
57
|
+
resolves the relative image references the README uses.
|
|
58
|
+
|
|
59
|
+
## [0.1.1] — 2026-06-20
|
|
60
|
+
|
|
61
|
+
First Action-driven publish. The 0.1.0 release happened via the
|
|
62
|
+
CLI fallback before the rubygems.org Trusted Publisher
|
|
63
|
+
registration was in place; 0.1.1 is the first version to land
|
|
64
|
+
through `release.yml`.
|
|
65
|
+
|
|
66
|
+
The user-visible code surface is intentionally tiny: the major
|
|
67
|
+
correctness fix (namespaced association resolution), the
|
|
68
|
+
Stats / Reachability / Edge perf rewrites, the SHA-pinned
|
|
69
|
+
workflows, the docs split, and the MIT `LICENSE.txt` all
|
|
70
|
+
already shipped in 0.1.0.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- `--version` / `-v` / `version` now prints
|
|
75
|
+
`rigor-module-graph X.Y.Z` instead of a bare `X.Y.Z`.
|
|
76
|
+
Matches the convention `bundler --version` and `gh --version`
|
|
77
|
+
use; bug reports pasted into chat are self-identifying.
|
|
78
|
+
|
|
20
79
|
## [0.1.0] — 2026-06-20
|
|
21
80
|
|
|
22
81
|
Initial release. Baseline shipping everything from the Phase 0
|
|
@@ -114,5 +173,7 @@ spike through Phase 5 (UML class diagram).
|
|
|
114
173
|
baseline and YJIT, and trailed baseline on Stats and
|
|
115
174
|
CycleDetector — recommendation stays YJIT.
|
|
116
175
|
|
|
117
|
-
[Unreleased]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.
|
|
176
|
+
[Unreleased]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.2...HEAD
|
|
177
|
+
[0.1.2]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.1...v0.1.2
|
|
178
|
+
[0.1.1]: https://github.com/nozomemein/rigor-module-graph/compare/v0.1.0...v0.1.1
|
|
118
179
|
[0.1.0]: https://github.com/nozomemein/rigor-module-graph/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# rigor-module-graph
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/rigor-module-graph)
|
|
4
|
+
[](LICENSE.txt)
|
|
5
|
+
[](https://github.com/nozomemein/rigor-module-graph/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/nozomemein/rigor-module-graph/actions/workflows/docs.yml)
|
|
7
|
+
|
|
3
8
|
Class/module/constant dependency graph for Ruby projects, built on
|
|
4
9
|
[Rigor](https://rigor.typedduck.fail/). The class-level counterpart
|
|
5
10
|
to Packwerk/Graphwerk: where those look at package boundaries, this
|
|
@@ -11,54 +16,7 @@ looks at the Ruby nominal graph — inheritance, `include`/`prepend`/
|
|
|
11
16
|
The screenshot above is from `examples/billing/`. Open
|
|
12
17
|
`examples/billing/index.html` for the live Mermaid version.
|
|
13
18
|
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
In principle this is a static-analysis tool that turns Ruby source
|
|
17
|
-
into a graph whose **nodes are classes / modules / constants** and
|
|
18
|
-
whose **edges are the references the language itself spells out**.
|
|
19
|
-
|
|
20
|
-
The pipeline:
|
|
21
|
-
|
|
22
|
-
1. Rigor parses Ruby into an AST with Prism.
|
|
23
|
-
2. The plugin's `node_rule`s pick up `ClassNode` / `CallNode` /
|
|
24
|
-
`ConstantReadNode` and friends.
|
|
25
|
-
3. Each interesting node becomes one or more edges:
|
|
26
|
-
- `class A < B` → `A -> B / inherits`
|
|
27
|
-
- `include M` → `A -> M / include`
|
|
28
|
-
- a `Money` constant reference → `A -> Money / const_ref`
|
|
29
|
-
(Phase 2 and later)
|
|
30
|
-
4. `from` is the lexical owner, assembled by walking
|
|
31
|
-
`context.ancestors` — so `class Billing::Invoice` produces
|
|
32
|
-
`Billing::Invoice`, not just `Invoice`.
|
|
33
|
-
5. `to` is resolved through a confidence ladder: syntax →
|
|
34
|
-
Zeitwerk convention → Rigor type information. Whatever we
|
|
35
|
-
couldn't pin down stays visible in the `confidence` field
|
|
36
|
-
rather than being dropped.
|
|
37
|
-
6. Every edge ships as a Rigor `:info` diagnostic. The `collect`
|
|
38
|
-
subcommand filters them on `rule == "edge"` and writes JSONL.
|
|
39
|
-
7. DOT, SVG, Mermaid, and cycle detection are all derived from
|
|
40
|
-
that JSONL.
|
|
41
|
-
|
|
42
|
-
So we are not watching what Ruby *does at runtime*. We're reading
|
|
43
|
-
Ruby's *named structure* and reconstructing, approximately, "which
|
|
44
|
-
constants depend on which other constants".
|
|
45
|
-
|
|
46
|
-
### This is not a call graph
|
|
47
|
-
|
|
48
|
-
We do not track who `foo.bar` resolves to at runtime. We track
|
|
49
|
-
the fact that the `Billing::Invoice` name depends on the
|
|
50
|
-
`ApplicationRecord` / `Auditable` / `Money` names. That is a
|
|
51
|
-
**nominal dependency graph** — a compiler-front-end-style view
|
|
52
|
-
of the project's syntactic and lexical structure, projected into
|
|
53
|
-
edges with explicit confidence.
|
|
54
|
-
|
|
55
|
-
Not re-implementing Ruby constant lookup is deliberate. For
|
|
56
|
-
understanding a Rails codebase's shape, it's more useful to leave
|
|
57
|
-
each edge tagged `syntax` / `zeitwerk` / `rigor_type` /
|
|
58
|
-
`unresolved` than to fake a `resolved` answer and silently get it
|
|
59
|
-
wrong.
|
|
60
|
-
|
|
61
|
-
## Installation
|
|
19
|
+
## Install
|
|
62
20
|
|
|
63
21
|
Via Bundler:
|
|
64
22
|
|
|
@@ -71,65 +29,61 @@ gem "rigor-module-graph"
|
|
|
71
29
|
bundle install
|
|
72
30
|
```
|
|
73
31
|
|
|
74
|
-
Or
|
|
32
|
+
Or system-wide:
|
|
75
33
|
|
|
76
34
|
```sh
|
|
77
35
|
gem install rigor-module-graph
|
|
78
36
|
```
|
|
79
37
|
|
|
80
|
-
Both paths pull in `rigortype` and `rbs ~> 4.0` transitively.
|
|
81
|
-
`rbs ~> 4.0` constraint is the
|
|
82
|
-
`RBS::Environment::ClassEntry#each_decl`, which
|
|
83
|
-
rbs 4.x. The Ruby 4.0 stdlib bundles rbs 3.10
|
|
84
|
-
so installing `rigor-module-graph` (which
|
|
85
|
-
makes RubyGems activate the 4.x gem at
|
|
86
|
-
analyzer stays alive.
|
|
87
|
-
|
|
88
|
-
## Configuration
|
|
38
|
+
Both paths pull in `rigortype` and `rbs ~> 4.0` transitively.
|
|
39
|
+
The `rbs ~> 4.0` constraint is the one that matters: rigortype
|
|
40
|
+
0.2.x calls `RBS::Environment::ClassEntry#each_decl`, which
|
|
41
|
+
only exists in rbs 4.x. The Ruby 4.0 stdlib bundles rbs 3.10
|
|
42
|
+
as a default gem, so installing `rigor-module-graph` (which
|
|
43
|
+
depends on rbs 4.x) makes RubyGems activate the 4.x gem at
|
|
44
|
+
run time and the analyzer stays alive.
|
|
89
45
|
|
|
90
|
-
|
|
46
|
+
For the full pipeline you also want `graphviz` installed so
|
|
47
|
+
`view --output svg` and `dot -Tsvg` can render PNG / SVG from
|
|
48
|
+
the generated DOT:
|
|
91
49
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- app
|
|
96
|
-
- lib
|
|
97
|
-
plugins:
|
|
98
|
-
- gem: rigor-module-graph
|
|
99
|
-
config:
|
|
100
|
-
rails_zeitwerk: true
|
|
101
|
-
autoload_paths:
|
|
102
|
-
- app/models
|
|
103
|
-
- app/controllers
|
|
104
|
-
- app/services
|
|
105
|
-
- app/jobs
|
|
106
|
-
- lib
|
|
107
|
-
concern_dirs:
|
|
108
|
-
- app/models/concerns
|
|
109
|
-
- app/controllers/concerns
|
|
110
|
-
include_constant_refs: false
|
|
50
|
+
```sh
|
|
51
|
+
brew install graphviz # macOS
|
|
52
|
+
apt-get install graphviz # Debian / Ubuntu
|
|
111
53
|
```
|
|
112
54
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`confidence: "syntax"` and skip path-based owner inference.
|
|
55
|
+
A working `dot` on `$PATH` is optional — text / Mermaid / HTML
|
|
56
|
+
output paths don't need it. See [How it works](docs/how-it-works.md)
|
|
57
|
+
for the pipeline overview.
|
|
117
58
|
|
|
118
|
-
##
|
|
119
|
-
|
|
120
|
-
### One-shot: `view`
|
|
59
|
+
## Getting started
|
|
121
60
|
|
|
122
|
-
The default subcommand analyses the current directory, writes
|
|
123
|
-
self-contained Mermaid HTML report under
|
|
124
|
-
and opens it in
|
|
125
|
-
project.
|
|
61
|
+
The default subcommand analyses the current directory, writes
|
|
62
|
+
a self-contained Mermaid HTML report under
|
|
63
|
+
`.rigor/module_graph/`, and opens it in a browser:
|
|
126
64
|
|
|
127
65
|
```sh
|
|
128
66
|
cd path/to/your/project
|
|
129
67
|
bundle exec rigor-module-graph # same as: rigor-module-graph view
|
|
130
68
|
```
|
|
131
69
|
|
|
132
|
-
|
|
70
|
+
A `.rigor.yml` must exist in the project root — that's how
|
|
71
|
+
`rigor` knows to load this plugin. The minimal version is two
|
|
72
|
+
lines:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
plugins:
|
|
76
|
+
- gem: rigor-module-graph
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That's enough for `view` to run with all defaults. Everything
|
|
80
|
+
else (`paths:`, `autoload_paths:`, …) goes in the
|
|
81
|
+
[Configuration](#configuration) section below, and every key
|
|
82
|
+
defaults to a sensible Rails-shaped value.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### `view` — one-shot HTML / SVG / Mermaid
|
|
133
87
|
|
|
134
88
|
```sh
|
|
135
89
|
# Don't open the browser (just write the HTML)
|
|
@@ -164,7 +118,7 @@ rigor-module-graph view --package
|
|
|
164
118
|
rigor-module-graph view --package-root /path/to/repo
|
|
165
119
|
```
|
|
166
120
|
|
|
167
|
-
`--direction` controls how the
|
|
121
|
+
`--direction` controls how the `--from` walk follows edges:
|
|
168
122
|
|
|
169
123
|
| direction | meaning |
|
|
170
124
|
|-----------|----------------------------------------|
|
|
@@ -179,17 +133,19 @@ rigor-module-graph view --package-root /path/to/repo
|
|
|
179
133
|
| `cluster` | keep every edge whose endpoints both fall in the reachable set (default — good for "show me the Article neighbourhood as a cluster") |
|
|
180
134
|
| `walk` | keep only the edges the BFS actually traversed (good for "show me what depends on Article and nothing else"; drops sibling edges like `Foo inherits ApplicationRecord` that just happen to share a base class with reachable nodes) |
|
|
181
135
|
|
|
182
|
-
A 1-hop `--from Article --direction out --edge-scope walk`
|
|
183
|
-
exactly the edges whose `from` is `Article`, never the
|
|
184
|
-
`inherits ApplicationRecord` of a reached node.
|
|
136
|
+
A 1-hop `--from Article --direction out --edge-scope walk`
|
|
137
|
+
returns exactly the edges whose `from` is `Article`, never the
|
|
138
|
+
sibling `inherits ApplicationRecord` of a reached node.
|
|
185
139
|
|
|
186
140
|
### Lower-level pipeline
|
|
187
141
|
|
|
188
|
-
The pipeline `view` runs is also exposed as discrete
|
|
189
|
-
when you want JSONL on disk or a pipeable text
|
|
142
|
+
The pipeline `view` runs is also exposed as discrete
|
|
143
|
+
subcommands when you want JSONL on disk or a pipeable text
|
|
144
|
+
output:
|
|
190
145
|
|
|
191
146
|
```sh
|
|
192
|
-
# Run `rigor check` and write edges JSONL
|
|
147
|
+
# Run `rigor check` and write edges JSONL
|
|
148
|
+
# (default: .rigor/module_graph/edges.jsonl)
|
|
193
149
|
bundle exec rigor-module-graph collect
|
|
194
150
|
|
|
195
151
|
# Render the graph
|
|
@@ -210,18 +166,19 @@ bundle exec rigor-module-graph class-diagram .rigor/module_graph/edges.jsonl > c
|
|
|
210
166
|
bundle exec rigor-module-graph class-diagram --no-private --no-attributes edges.jsonl
|
|
211
167
|
```
|
|
212
168
|
|
|
213
|
-
`collect` shells out to `rigor check --format json --no-cache`
|
|
214
|
-
filters diagnostics on
|
|
215
|
-
`
|
|
216
|
-
|
|
169
|
+
`collect` shells out to `rigor check --format json --no-cache`
|
|
170
|
+
and filters diagnostics on
|
|
171
|
+
`source_family == "plugin.module-graph"` + `rule == "edge"`,
|
|
172
|
+
so re-running is deterministic and there's no on-disk
|
|
173
|
+
side-effect from the plugin itself.
|
|
217
174
|
|
|
218
175
|
`dot` / `mermaid` / `cycles` accept a file argument or read stdin.
|
|
219
176
|
|
|
220
177
|
### Filters and collapse
|
|
221
178
|
|
|
222
|
-
All
|
|
223
|
-
|
|
224
|
-
|
|
179
|
+
All reader subcommands accept the same filter flags. They prune
|
|
180
|
+
the edge set before rendering / detecting; the JSONL on disk is
|
|
181
|
+
untouched.
|
|
225
182
|
|
|
226
183
|
```sh
|
|
227
184
|
# Drop noisy const_ref / unresolved edges
|
|
@@ -230,7 +187,8 @@ bundle exec rigor-module-graph dot --kind inherits,include,prepend,extend edges.
|
|
|
230
187
|
# Only the edges we're sure about
|
|
231
188
|
bundle exec rigor-module-graph dot --confidence syntax,zeitwerk,rigor_type edges.jsonl
|
|
232
189
|
|
|
233
|
-
# Fold every Billing::* node into one cluster
|
|
190
|
+
# Fold every Billing::* node into one cluster
|
|
191
|
+
# (Dot: subgraph_cluster_; Mermaid: subgraph)
|
|
234
192
|
bundle exec rigor-module-graph dot --collapse Billing,Auth edges.jsonl
|
|
235
193
|
bundle exec rigor-module-graph mermaid --collapse Billing edges.jsonl
|
|
236
194
|
|
|
@@ -247,6 +205,46 @@ bundle exec rigor-module-graph mermaid --package-root /path/to/repo edges.jsonl
|
|
|
247
205
|
bundle exec rigor-module-graph cycles --kind inherits,include edges.jsonl
|
|
248
206
|
```
|
|
249
207
|
|
|
208
|
+
## Configuration
|
|
209
|
+
|
|
210
|
+
`.rigor.yml` lives in the project root and is **required** —
|
|
211
|
+
`rigor` reads it to discover this plugin. `rigor init` scaffolds
|
|
212
|
+
a `.rigor.dist.yml` you can rename, or write it by hand. The
|
|
213
|
+
two-line minimum from [Getting started](#getting-started) is
|
|
214
|
+
enough; the full form below is for tuning.
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
target_ruby: '4.0'
|
|
218
|
+
paths:
|
|
219
|
+
- app
|
|
220
|
+
- lib
|
|
221
|
+
plugins:
|
|
222
|
+
- gem: rigor-module-graph
|
|
223
|
+
config:
|
|
224
|
+
rails_zeitwerk: true
|
|
225
|
+
autoload_paths:
|
|
226
|
+
- app/models
|
|
227
|
+
- app/controllers
|
|
228
|
+
- app/services
|
|
229
|
+
- app/jobs
|
|
230
|
+
- lib
|
|
231
|
+
concern_dirs:
|
|
232
|
+
- app/models/concerns
|
|
233
|
+
- app/controllers/concerns
|
|
234
|
+
include_constant_refs: false
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Every key shown is the default. Two switches worth knowing:
|
|
238
|
+
|
|
239
|
+
- `include_constant_refs: true` — emit `const_ref` edges from
|
|
240
|
+
bare constant references inside method bodies. Off by default
|
|
241
|
+
because the volume of edges grows fast on a typical Rails app
|
|
242
|
+
and the noise can drown the structural picture.
|
|
243
|
+
- `rails_zeitwerk: false` — keep every edge at
|
|
244
|
+
`confidence: "syntax"` and skip the path-based owner
|
|
245
|
+
inference. Useful when the project doesn't follow Zeitwerk's
|
|
246
|
+
autoload convention.
|
|
247
|
+
|
|
250
248
|
## Edge format
|
|
251
249
|
|
|
252
250
|
Each edge in the JSONL file looks like:
|
|
@@ -279,6 +277,10 @@ published to GitHub Pages on every push to `main`.
|
|
|
279
277
|
built from `main`, mirrors the current source.
|
|
280
278
|
- [API reference (RubyGems)](https://rubydoc.info/gems/rigor-module-graph) —
|
|
281
279
|
the last released gem on rubydoc.info.
|
|
280
|
+
- [How it works](docs/how-it-works.md) — the static-analysis
|
|
281
|
+
pipeline (Prism → node rules → confidence ladder → JSONL →
|
|
282
|
+
renderers), and why this is a nominal dependency graph and
|
|
283
|
+
not a call graph.
|
|
282
284
|
- [Development guide](docs/development.md) — local setup, git
|
|
283
285
|
hooks, CI / Release workflows, test suite layout.
|
|
284
286
|
- [Design plan](docs/plan.md) — the decisions still
|
|
@@ -16,6 +16,7 @@ require_relative "reachability"
|
|
|
16
16
|
require_relative "stats"
|
|
17
17
|
require_relative "packwerk_overlay"
|
|
18
18
|
require_relative "html_view"
|
|
19
|
+
require_relative "status_reporter"
|
|
19
20
|
require_relative "uml/class_diagram"
|
|
20
21
|
|
|
21
22
|
module Rigor
|
|
@@ -66,7 +67,7 @@ module Rigor
|
|
|
66
67
|
stdout.puts USAGE
|
|
67
68
|
0
|
|
68
69
|
when "version", "-v", "--version"
|
|
69
|
-
stdout.puts Rigor::ModuleGraph::VERSION
|
|
70
|
+
stdout.puts "rigor-module-graph #{Rigor::ModuleGraph::VERSION}"
|
|
70
71
|
0
|
|
71
72
|
else
|
|
72
73
|
stderr.puts "rigor-module-graph: unknown command #{command.inspect}"
|
|
@@ -255,6 +256,7 @@ module Rigor
|
|
|
255
256
|
output: DEFAULT_EDGES_PATH,
|
|
256
257
|
nodes_output: DEFAULT_NODES_PATH,
|
|
257
258
|
cache: false,
|
|
259
|
+
quiet: false,
|
|
258
260
|
rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor")
|
|
259
261
|
}
|
|
260
262
|
end
|
|
@@ -263,11 +265,15 @@ module Rigor
|
|
|
263
265
|
parser = build_parser
|
|
264
266
|
paths = parser.parse(argv)
|
|
265
267
|
|
|
268
|
+
status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet])
|
|
269
|
+
|
|
266
270
|
ensure_output_dirs
|
|
267
271
|
runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache])
|
|
268
|
-
edges, nodes = runner.analyse(paths)
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) }
|
|
273
|
+
status.info "#{edges.size} edge(s), #{nodes.size} node(s)"
|
|
274
|
+
status.step("Writing #{@options[:output]}") { write_edges(edges) }
|
|
275
|
+
status.step("Writing #{@options[:nodes_output]}") { write_nodes(nodes) }
|
|
276
|
+
|
|
271
277
|
@stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{@options[:output]}, " \
|
|
272
278
|
"#{nodes.size} node(s) to #{@options[:nodes_output]}"
|
|
273
279
|
0
|
|
@@ -279,6 +285,13 @@ module Rigor
|
|
|
279
285
|
1
|
|
280
286
|
end
|
|
281
287
|
|
|
288
|
+
# Path-aware label so the user can see which paths Rigor
|
|
289
|
+
# is being pointed at when the step is slow.
|
|
290
|
+
def rigor_step_label(paths)
|
|
291
|
+
target = paths.empty? ? "configured paths" : paths.join(", ")
|
|
292
|
+
"Running rigor check on #{target}"
|
|
293
|
+
end
|
|
294
|
+
|
|
282
295
|
def build_parser
|
|
283
296
|
OptionParser.new do |opts|
|
|
284
297
|
opts.banner = "Usage: rigor-module-graph collect [options] [PATHS...]"
|
|
@@ -298,6 +311,9 @@ module Rigor
|
|
|
298
311
|
"Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd|
|
|
299
312
|
@options[:rigor_cmd] = cmd
|
|
300
313
|
end
|
|
314
|
+
opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do
|
|
315
|
+
@options[:quiet] = true
|
|
316
|
+
end
|
|
301
317
|
opts.on("-h", "--help") do
|
|
302
318
|
@stdout.puts opts
|
|
303
319
|
exit 0
|
|
@@ -365,6 +381,7 @@ module Rigor
|
|
|
365
381
|
format: "html",
|
|
366
382
|
output: nil,
|
|
367
383
|
cache: false,
|
|
384
|
+
quiet: false,
|
|
368
385
|
rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor"),
|
|
369
386
|
open: true,
|
|
370
387
|
collapse: nil,
|
|
@@ -385,22 +402,34 @@ module Rigor
|
|
|
385
402
|
parser = build_parser
|
|
386
403
|
paths = parser.parse(argv)
|
|
387
404
|
|
|
405
|
+
status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet])
|
|
406
|
+
|
|
388
407
|
runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache])
|
|
389
|
-
edges, nodes = runner.analyse(paths)
|
|
390
|
-
edges
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) }
|
|
409
|
+
status.info "#{edges.size} edge(s), #{nodes.size} node(s)"
|
|
410
|
+
|
|
411
|
+
if any_filter_active?
|
|
412
|
+
edges = status.step("Applying filters") do
|
|
413
|
+
apply_filters(
|
|
414
|
+
edges,
|
|
415
|
+
kinds: @options[:kinds],
|
|
416
|
+
confidences: @options[:confidences],
|
|
417
|
+
from: @options[:from],
|
|
418
|
+
depth: @options[:depth],
|
|
419
|
+
direction: @options[:direction],
|
|
420
|
+
edge_scope: @options[:edge_scope]
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
status.info "#{edges.size} edge(s) after filters"
|
|
424
|
+
end
|
|
425
|
+
|
|
399
426
|
groups = package_groups(edges)
|
|
400
427
|
collapse = groups ? [] : effective_collapse(edges)
|
|
401
428
|
|
|
402
|
-
payload, binary =
|
|
403
|
-
|
|
429
|
+
payload, binary = status.step("Rendering #{@options[:format]}") do
|
|
430
|
+
render_payload(edges, nodes, collapse, groups)
|
|
431
|
+
end
|
|
432
|
+
deliver(payload, binary: binary, edges: edges, status: status)
|
|
404
433
|
0
|
|
405
434
|
rescue OptionParser::ParseError => e
|
|
406
435
|
@stderr.puts "rigor-module-graph view: #{e.message}"
|
|
@@ -410,6 +439,20 @@ module Rigor
|
|
|
410
439
|
1
|
|
411
440
|
end
|
|
412
441
|
|
|
442
|
+
def rigor_step_label(paths)
|
|
443
|
+
target = paths.empty? ? "configured paths" : paths.join(", ")
|
|
444
|
+
"Running rigor check on #{target}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def any_filter_active?
|
|
448
|
+
@options[:kinds] || @options[:confidences] ||
|
|
449
|
+
@options[:from] || @options[:depth]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def silent_status
|
|
453
|
+
Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: true)
|
|
454
|
+
end
|
|
455
|
+
|
|
413
456
|
class RenderError < StandardError; end
|
|
414
457
|
|
|
415
458
|
# Builds the rendered payload for the chosen format and
|
|
@@ -475,7 +518,10 @@ module Rigor
|
|
|
475
518
|
|
|
476
519
|
# Writes the payload to the configured destination and
|
|
477
520
|
# opens the browser when the html-default flow applies.
|
|
478
|
-
|
|
521
|
+
# `status:` defaults to a silent reporter so the existing
|
|
522
|
+
# test surface (which exercises `deliver` directly) keeps
|
|
523
|
+
# working without threading a reporter through.
|
|
524
|
+
def deliver(payload, binary:, edges:, status: silent_status)
|
|
479
525
|
destination = effective_output_path
|
|
480
526
|
if destination.nil?
|
|
481
527
|
if binary
|
|
@@ -485,12 +531,16 @@ module Rigor
|
|
|
485
531
|
return
|
|
486
532
|
end
|
|
487
533
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
534
|
+
status.step("Writing #{destination}") do
|
|
535
|
+
dir = File.dirname(destination)
|
|
536
|
+
FileUtils.mkdir_p(dir) unless dir.empty? || dir == "."
|
|
537
|
+
mode = binary ? "wb" : "w"
|
|
538
|
+
File.open(destination, mode) { |io| io.write(payload) }
|
|
539
|
+
end
|
|
492
540
|
@stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{destination}"
|
|
493
|
-
|
|
541
|
+
return unless html? && @options[:open]
|
|
542
|
+
|
|
543
|
+
status.step("Opening #{destination} in browser") { open_in_browser(destination) }
|
|
494
544
|
end
|
|
495
545
|
|
|
496
546
|
# Resolve the output path. `-o PATH` always wins. With no
|
|
@@ -563,6 +613,9 @@ module Rigor
|
|
|
563
613
|
"Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd|
|
|
564
614
|
@options[:rigor_cmd] = cmd
|
|
565
615
|
end
|
|
616
|
+
opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do
|
|
617
|
+
@options[:quiet] = true
|
|
618
|
+
end
|
|
566
619
|
add_filter_options(opts, @options)
|
|
567
620
|
opts.on("-h", "--help") do
|
|
568
621
|
@stdout.puts opts
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module ModuleGraph
|
|
5
|
+
# Step-level progress reporter that prints to stderr.
|
|
6
|
+
#
|
|
7
|
+
# On a TTY the message + elapsed time render inline on a
|
|
8
|
+
# single line ("==> Running rigor check... done (4.32s)").
|
|
9
|
+
# When stderr is redirected (CI logs, piping into another
|
|
10
|
+
# command, `tee` to a file) both halves print on separate
|
|
11
|
+
# lines so the output stays line-oriented and grep-friendly.
|
|
12
|
+
#
|
|
13
|
+
# `quiet: true` silences every method; callers can wire a
|
|
14
|
+
# `--quiet` CLI flag through without litterring conditionals
|
|
15
|
+
# at each call site.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
#
|
|
19
|
+
# status = StatusReporter.new(stderr: $stderr)
|
|
20
|
+
# edges = status.step("Running rigor check") do
|
|
21
|
+
# runner.edges_for(paths)
|
|
22
|
+
# end
|
|
23
|
+
# status.info "#{edges.size} edges"
|
|
24
|
+
class StatusReporter
|
|
25
|
+
def initialize(stderr:, quiet: false)
|
|
26
|
+
@stderr = stderr
|
|
27
|
+
@quiet = quiet
|
|
28
|
+
@tty = stderr.respond_to?(:tty?) && stderr.tty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Print a "==> message..." line, yield, then print the
|
|
32
|
+
# outcome ("done (Xms)" or "failed") with elapsed time.
|
|
33
|
+
# Returns whatever the block returns; re-raises on
|
|
34
|
+
# exception after printing the failure tail so callers can
|
|
35
|
+
# still rescue normally.
|
|
36
|
+
def step(message)
|
|
37
|
+
return yield if @quiet
|
|
38
|
+
|
|
39
|
+
start_step(message)
|
|
40
|
+
started_at = monotonic
|
|
41
|
+
begin
|
|
42
|
+
result = yield
|
|
43
|
+
rescue StandardError
|
|
44
|
+
finish_step("failed", monotonic - started_at)
|
|
45
|
+
raise
|
|
46
|
+
end
|
|
47
|
+
finish_step("done", monotonic - started_at)
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Print an informational line indented under the most
|
|
52
|
+
# recent step. Used for "2016 edges, 87 nodes" style
|
|
53
|
+
# post-step counts.
|
|
54
|
+
def info(message)
|
|
55
|
+
return if @quiet
|
|
56
|
+
|
|
57
|
+
@stderr.puts " #{message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def start_step(message)
|
|
63
|
+
prefix = "==> #{message}"
|
|
64
|
+
if @tty
|
|
65
|
+
@stderr.print "#{prefix}... "
|
|
66
|
+
@stderr.flush
|
|
67
|
+
else
|
|
68
|
+
@stderr.puts prefix
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def finish_step(verb, elapsed)
|
|
73
|
+
duration = format_duration(elapsed)
|
|
74
|
+
if @tty
|
|
75
|
+
@stderr.puts "#{verb} #{duration}"
|
|
76
|
+
else
|
|
77
|
+
@stderr.puts " #{verb} #{duration}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def monotonic
|
|
82
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_duration(seconds)
|
|
86
|
+
if seconds < 1
|
|
87
|
+
"(#{(seconds * 1000).round}ms)"
|
|
88
|
+
else
|
|
89
|
+
"(#{seconds.round(2)}s)"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/rigor-module-graph.rb
CHANGED
|
@@ -29,4 +29,5 @@ require_relative "rigor/module_graph/stats"
|
|
|
29
29
|
require_relative "rigor/module_graph/packwerk_overlay"
|
|
30
30
|
require_relative "rigor/module_graph/uml/class_diagram"
|
|
31
31
|
require_relative "rigor/module_graph/html_view"
|
|
32
|
+
require_relative "rigor/module_graph/status_reporter"
|
|
32
33
|
require_relative "rigor/module_graph/plugin"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigor-module-graph
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nozomi Hijikata
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- lib/rigor/module_graph/plugin/rigor_plugin.rb
|
|
71
71
|
- lib/rigor/module_graph/reachability.rb
|
|
72
72
|
- lib/rigor/module_graph/stats.rb
|
|
73
|
+
- lib/rigor/module_graph/status_reporter.rb
|
|
73
74
|
- lib/rigor/module_graph/templates/view.html.erb
|
|
74
75
|
- lib/rigor/module_graph/uml/class_diagram.rb
|
|
75
76
|
- lib/rigor/module_graph/version.rb
|