Class: Rigor::ModuleGraph::CLI::View
- Inherits:
-
Object
- Object
- Rigor::ModuleGraph::CLI::View
- Includes:
- EdgeFilters
- Defined in:
- lib/rigor/module_graph/cli.rb
Overview
‘view` is the one-shot entry point: from the project root type `rigor-module-graph` and it analyses the current directory, writes a self-contained Mermaid HTML report, and opens it in a browser.
Defaults are tuned to need zero flags on a Rails-shaped project. The lower-level subcommands (collect / dot / mermaid) stay available for piped use.
Defined Under Namespace
Classes: RenderError
Constant Summary collapse
- DEFAULT_OUTPUT =
".rigor/module_graph/view.html"- AUTO_COLLAPSE_THRESHOLD =
An auto-collapsed cluster needs at least this many members before it’s worth folding. Three is the sweet spot empirically: a 1500-edge Rails app collapses into roughly the right shape, and a small fixture still leaves trivial Foo / Bar pairs uncollapsed.
3- SUBTITLE_COLLAPSE_PREVIEW =
Cap the visible “collapsed: …” trailer in the subtitle so it doesn’t grow into an unreadable wall on large projects.
6- FORMATS =
The supported output formats, in roughly increasing “wrapping” order: html embeds mermaid; svg embeds dot; the rest are raw text.
%w[html mermaid dot svg class-diagram].freeze
- DEFAULT_HTML_OUTPUT =
Default file destination when format is html and the user didn’t override with -o. Non-html formats default to stdout.
".rigor/module_graph/view.html"
Constants included from EdgeFilters
EdgeFilters::VALID_CONFIDENCES, EdgeFilters::VALID_DIRECTIONS, EdgeFilters::VALID_EDGE_SCOPES, EdgeFilters::VALID_KINDS
Instance Method Summary collapse
- #any_filter_active? ⇒ Boolean
- #build_parser ⇒ Object
-
#deliver(payload, binary:, edges:, status: silent_status) ⇒ Object
Writes the payload to the configured destination and opens the browser when the html-default flow applies.
-
#effective_collapse(edges) ⇒ Object
Choose collapse prefixes.
-
#effective_output_path ⇒ Object
Resolve the output path.
-
#graphviz_svg(dot_source) ⇒ Object
Shell out to Graphviz ‘dot -Tsvg`.
- #html? ⇒ Boolean
-
#initialize(stdout:, stderr:) ⇒ View
constructor
A new instance of View.
- #open_in_browser(path) ⇒ Object
- #package_groups(edges) ⇒ Object
-
#render_payload(edges, nodes, collapse, groups) ⇒ Object
Builds the rendered payload for the chosen format and signals whether the bytes are binary (svg via Graphviz can return a non-UTF-8 image stream).
- #render_subtitle(edges, collapse, groups) ⇒ Object
-
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
When the user narrows the edge set with ‘–from` / `–kind` / `–confidence`, the class diagram should only show classes that participate in those edges — otherwise every constant declared in the project still shows up as a body-less class.
- #rigor_step_label(paths) ⇒ Object
- #run(argv) ⇒ Object
- #silent_status ⇒ Object
Methods included from EdgeFilters
#add_filter_options, #apply_filters, #validate!
Constructor Details
#initialize(stdout:, stderr:) ⇒ View
Returns a new instance of View.
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
# File 'lib/rigor/module_graph/cli.rb', line 377 def initialize(stdout:, stderr:) @stdout = stdout @stderr = stderr @options = { format: "html", output: nil, cache: false, quiet: false, rigor_cmd: ENV.fetch("RIGOR_CMD", "rigor"), open: true, collapse: nil, kinds: nil, confidences: nil, from: nil, depth: nil, direction: :both, edge_scope: :cluster, package: nil, include_methods: true, include_attributes: true, visibilities: %w[public protected private] } end |
Instance Method Details
#any_filter_active? ⇒ Boolean
447 448 449 450 |
# File 'lib/rigor/module_graph/cli.rb', line 447 def any_filter_active? @options[:kinds] || @options[:confidences] || @options[:from] || @options[:depth] end |
#build_parser ⇒ Object
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 |
# File 'lib/rigor/module_graph/cli.rb', line 560 def build_parser OptionParser.new do |opts| opts. = "Usage: rigor-module-graph view [options] [PATHS...]" opts.on("--output FORMAT", FORMATS, "Output format (#{FORMATS.join("|")}; default: html). " \ "Non-html streams to stdout unless -o is given.") do |fmt| @options[:format] = fmt end opts.on("-o", "--save PATH", "Write to PATH instead of stdout / the default html location") do |path| @options[:output] = path end opts.on("--[no-]open", "Open the html in a browser (default: true; ignored for non-html)") do |flag| @options[:open] = flag end opts.on("--collapse PREFIXES", Array, "Manual collapse list (disables auto-detection)") do |prefixes| @options[:collapse] = prefixes end opts.on("--no-collapse", "Disable namespace collapse entirely") do @options[:collapse] = [] end opts.on("--no-methods", "[class-diagram] Don't render methods inside class bodies") do @options[:include_methods] = false end opts.on("--no-attributes", "[class-diagram] Don't render attributes inside class bodies") do @options[:include_attributes] = false end opts.on("--public-only", "[class-diagram] Only show public members") do @options[:visibilities] = %w[public] end opts.on("--no-private", "[class-diagram] Hide private members") do @options[:visibilities] = %w[public protected] end opts.on("--package", "Cluster by Packwerk packages discovered in cwd") do @options[:package] ||= "." end opts.on("--package-root PATH", "Cluster by Packwerk packages discovered under PATH") do |root| @options[:package] = root end opts.on("--[no-]cache", "Pass --cache / --no-cache to rigor (default: --no-cache)") do |cache| @options[:cache] = cache end opts.on("--rigor-cmd CMD", "Override the rigor binary (default: rigor or $RIGOR_CMD)") do |cmd| @options[:rigor_cmd] = cmd end opts.on("-q", "--quiet", "Suppress step-level progress on stderr") do @options[:quiet] = true end (opts, @options) opts.on("-h", "--help") do @stdout.puts opts exit 0 end end end |
#deliver(payload, binary:, edges:, status: silent_status) ⇒ Object
Writes the payload to the configured destination and opens the browser when the html-default flow applies. ‘status:` defaults to a silent reporter so the existing test surface (which exercises `deliver` directly) keeps working without threading a reporter through.
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 |
# File 'lib/rigor/module_graph/cli.rb', line 524 def deliver(payload, binary:, edges:, status: silent_status) destination = effective_output_path if destination.nil? if binary @stdout.binmode end @stdout.write(payload) return end status.step("Writing #{destination}") do dir = File.dirname(destination) FileUtils.mkdir_p(dir) unless dir.empty? || dir == "." mode = binary ? "wb" : "w" File.open(destination, mode) { |io| io.write(payload) } end @stderr.puts "rigor-module-graph: wrote #{edges.size} edge(s) to #{destination}" return unless html? && @options[:open] status.step("Opening #{destination} in browser") { open_in_browser(destination) } end |
#effective_collapse(edges) ⇒ Object
Choose collapse prefixes. Explicit ‘–collapse` wins; otherwise we auto-pick top-level namespaces that have at least AUTO_COLLAPSE_THRESHOLD distinct nodes under them, which is what most graphs benefit from.
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 |
# File 'lib/rigor/module_graph/cli.rb', line 631 def effective_collapse(edges) return @options[:collapse] unless @options[:collapse].nil? counts = Hash.new { |h, k| h[k] = Set.new } edges.each do |edge| [edge.from, edge.to].each do |name| head, tail = name.split("::", 2) # Only collapse on the top-level segment so a deep # tree like `Billing::Invoice::Line` still feeds into # the `Billing` cluster — picking inner prefixes # would compete with each other and produce nested # clusters that hurt readability. next if tail.nil? || tail.empty? # Absolute paths (`::Foo::Bar`) split with an empty # head; skip them so they don't surface as the bogus # `""` collapse target. next if head.empty? counts[head] << name end end counts.select { |_, members| members.size >= AUTO_COLLAPSE_THRESHOLD }.keys.sort end |
#effective_output_path ⇒ Object
Resolve the output path. ‘-o PATH` always wins. With no explicit path, html falls back to `.rigor/module_graph/ view.html`; every other format streams to stdout.
549 550 551 552 553 554 |
# File 'lib/rigor/module_graph/cli.rb', line 549 def effective_output_path return @options[:output] if @options[:output] return DEFAULT_HTML_OUTPUT if html? nil end |
#graphviz_svg(dot_source) ⇒ Object
Shell out to Graphviz ‘dot -Tsvg`. Surfacing the binary check as a clear error keeps the message friendlier than the raw `Errno::ENOENT` Open3 would propagate.
507 508 509 510 511 512 513 514 515 516 517 |
# File 'lib/rigor/module_graph/cli.rb', line 507 def graphviz_svg(dot_source) stdout_str, stderr_str, status = Open3.capture3("dot", "-Tsvg", stdin_data: dot_source) unless status.success? raise RenderError, "graphviz `dot` failed (exit #{status.exitstatus}): #{stderr_str}" end stdout_str rescue Errno::ENOENT raise RenderError, "graphviz `dot` not found on PATH; install via " \ "`brew install graphviz` (macOS) or your distro's package manager" end |
#html? ⇒ Boolean
556 557 558 |
# File 'lib/rigor/module_graph/cli.rb', line 556 def html? @options[:format] == "html" end |
#open_in_browser(path) ⇒ Object
693 694 695 696 697 698 699 |
# File 'lib/rigor/module_graph/cli.rb', line 693 def open_in_browser(path) opener = ENV["BROWSER"] || (RUBY_PLATFORM.include?("darwin") ? "open" : "xdg-open") system(opener, path) rescue StandardError => e @stderr.puts "rigor-module-graph view: could not open #{path}: #{e.}" end |
#package_groups(edges) ⇒ Object
680 681 682 683 684 685 686 687 688 689 690 691 |
# File 'lib/rigor/module_graph/cli.rb', line 680 def package_groups(edges) return nil unless @options[:package] = PackwerkOverlay.discover(@options[:package]) unless .any? @stderr.puts "rigor-module-graph view: no package.yml found under " \ "#{@options[:package].inspect}; falling back to namespace collapse" return nil end .groups_for(edges) end |
#render_payload(edges, nodes, collapse, groups) ⇒ Object
Builds the rendered payload for the chosen format and signals whether the bytes are binary (svg via Graphviz can return a non-UTF-8 image stream).
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/rigor/module_graph/cli.rb', line 461 def render_payload(edges, nodes, collapse, groups) case @options[:format] when "html" mermaid = Mermaid.render(edges, collapse: collapse, groups: groups) html = HtmlView.render( title: "rigor-module-graph: #{File.basename(Dir.pwd)}", subtitle: render_subtitle(edges, collapse, groups), mermaid_source: mermaid ) [html, false] when "mermaid" [Mermaid.render(edges, collapse: collapse, groups: groups), false] when "dot" [Dot.render(edges, collapse: collapse, groups: groups), false] when "svg" [graphviz_svg(Dot.render(edges, collapse: collapse, groups: groups)), true] when "class-diagram" [ Uml::ClassDiagram.render( edges, restrict_nodes_to_edges(nodes, edges), include_methods: @options[:include_methods], include_attributes: @options[:include_attributes], visibilities: @options[:visibilities] ), false ] end end |
#render_subtitle(edges, collapse, groups) ⇒ Object
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 |
# File 'lib/rigor/module_graph/cli.rb', line 655 def render_subtitle(edges, collapse, groups) parts = ["#{edges.size} edge(s) from #{Dir.pwd}"] if @options[:from] from_part = +"from: #{Array(@options[:from]).join(", ")}" from_part << " (depth=#{@options[:depth]})" if @options[:depth] from_part << " [#{@options[:direction]}]" unless @options[:direction] == :both parts << from_part end if groups uniq_packages = groups.values.uniq.sort preview = uniq_packages.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"packages: #{preview.join(", ")}" if uniq_packages.size > preview.size label << " (+#{uniq_packages.size - preview.size} more)" end parts << label elsif !collapse.empty? preview = collapse.first(SUBTITLE_COLLAPSE_PREVIEW) label = +"collapsed: #{preview.join(", ")}" label << " (+#{collapse.size - preview.size} more)" if collapse.size > preview.size parts << label end parts.join(" · ") end |
#restrict_nodes_to_edges(nodes, edges) ⇒ Object
When the user narrows the edge set with ‘–from` / `–kind` / `–confidence`, the class diagram should only show classes that participate in those edges — otherwise every constant declared in the project still shows up as a body-less class. The filter is a no-op when the edge set already covers every node (no filters applied).
496 497 498 499 500 501 502 |
# File 'lib/rigor/module_graph/cli.rb', line 496 def restrict_nodes_to_edges(nodes, edges) return nodes if edges.empty? visible = Set.new edges.each { |edge| visible << edge.from << edge.to } nodes.select { |node| visible.include?(node.owner) || visible.include?(node.name) } end |
#rigor_step_label(paths) ⇒ Object
442 443 444 445 |
# File 'lib/rigor/module_graph/cli.rb', line 442 def rigor_step_label(paths) target = paths.empty? ? "configured paths" : paths.join(", ") "Running rigor check on #{target}" end |
#run(argv) ⇒ Object
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
# File 'lib/rigor/module_graph/cli.rb', line 401 def run(argv) parser = build_parser paths = parser.parse(argv) status = Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: @options[:quiet]) runner = RigorRunner.new(rigor_cmd: @options[:rigor_cmd], cache: @options[:cache]) edges, nodes = status.step(rigor_step_label(paths)) { runner.analyse(paths) } status.info "#{edges.size} edge(s), #{nodes.size} node(s)" if any_filter_active? edges = status.step("Applying filters") do apply_filters( edges, kinds: @options[:kinds], confidences: @options[:confidences], from: @options[:from], depth: @options[:depth], direction: @options[:direction], edge_scope: @options[:edge_scope] ) end status.info "#{edges.size} edge(s) after filters" end groups = package_groups(edges) collapse = groups ? [] : effective_collapse(edges) payload, binary = status.step("Rendering #{@options[:format]}") do render_payload(edges, nodes, collapse, groups) end deliver(payload, binary: binary, edges: edges, status: status) 0 rescue OptionParser::ParseError => e @stderr.puts "rigor-module-graph view: #{e.}" 2 rescue CollectError, RenderError => e @stderr.puts "rigor-module-graph view: #{e.}" 1 end |
#silent_status ⇒ Object
452 453 454 |
# File 'lib/rigor/module_graph/cli.rb', line 452 def silent_status Rigor::ModuleGraph::StatusReporter.new(stderr: @stderr, quiet: true) end |