Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Codemod v3 slots improvements #1746

Merged
merged 16 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ nav_order: 5

*Joseph Carpenter*

* Add codemod to detect and migrate deprecated Slots setters to new `with_*` prefix introduced in v3.x. Note: This codemod is non-deterministic and works on a best-effort basis.

```bash
bin/rails view_component:detect_legacy_slots
bin/rails view_component:migrate_legacy_slots
Spone marked this conversation as resolved.
Show resolved Hide resolved
```

*Hans Lemuet, Kirill Platonov*

### v3.0.0

1,000+ days and 100+ releases later, the 200+ contributors to ViewComponent are proud to ship v3.0.0!
Expand Down
15 changes: 15 additions & 0 deletions lib/tasks/view_component.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "view_component/codemods/v3_slot_setters"

namespace :view_component do
task detect_legacy_slots: :environment do
ARGV.each { |a| task a.to_sym {} }
custom_paths = ARGV.compact.map { |path| Rails.root.join(path) }
ViewComponent::Codemods::V3SlotSetters.new(view_path: custom_paths).call
end

task migrate_legacy_slots: :environment do
ARGV.each { |a| task a.to_sym {} }
custom_paths = ARGV.compact.map { |path| Rails.root.join(path) }
ViewComponent::Codemods::V3SlotSetters.new(view_path: custom_paths, migrate: true).call
end
end
86 changes: 63 additions & 23 deletions lib/view_component/codemods/v3_slot_setters.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
# Usage (in rails console):
#
# Run the codemod:
#
# ViewComponent::Codemods::V3SlotSetters.new.call
#
# If your app uses custom paths for views, you can pass them in:
#
# ViewComponent::Codemods::V3SlotSetters.new(
# view_path: "../app/views",
# ).call
# frozen_string_literal: true

module ViewComponent
# Usage:
#
# Run via rake task:
#
# bin/rails view_component:detect_legacy_slots
# bin/rails view_component:migrate_legacy_slots
# bin/rails view_component:migrate_legacy_slots app/views
#
# Or run via rails console if you need to pass custom paths:
#
# ViewComponent::Codemods::V3SlotSetters.new(
# view_path: Rails.root.join("app/views"),
# ).call
module Codemods
class V3SlotSetters
TEMPLATE_LANGUAGES = %w[erb slim haml].join(",").freeze
RENDER_REGEX = /render[( ](?<component>\w+(?:::\w+)*)\.new[) ]+(do|\{) \|(?<arg>\w+)\b/ # standard:disable Lint/MixedRegexpCaptureTypes

Suggestion = Struct.new(:file, :line, :message)

def initialize(view_component_path: [], view_path: [])
Zeitwerk::Loader.eager_load_all
def initialize(view_component_path: [], view_path: [], migrate: false)
Rails.application.eager_load!
Spone marked this conversation as resolved.
Show resolved Hide resolved

@view_component_path = view_component_path
@view_path = view_path
@migrate = migrate
end

def call
Expand Down Expand Up @@ -72,36 +76,65 @@ def scan_exact_matches(file)
end
end

File.open(file) do |f|
File.open(file, "r+") do |f|
lines = []
f.each_line do |line|
rendered_components.each do |rendered_component|
arg = rendered_component[:arg]
slots = rendered_component[:slots]

if (matches = line.scan(/#{arg}\.#{Regexp.union(slots)}/))
matches.each do |match|
suggestions << Suggestion.new(file, f.lineno, "probably replace `#{match}` with `#{match.gsub("#{arg}.", "#{arg}.with_")}`")
new_value = match.gsub("#{arg}.", "#{arg}.with_")
message = if @migrate
"replaced `#{match}` with `#{new_value}`"
else
"probably replace `#{match}` with `#{new_value}`"
end
suggestions << Suggestion.new(file, f.lineno, message)
if @migrate
line.gsub!("#{arg}.", "#{arg}.with_")
end
end
end
end
lines << line
end

if @migrate
f.rewind
f.write(lines.join)
end
end
end
end

def scan_uncertain_matches(file)
[].tap do |suggestions|
File.open(file) do |f|
File.open(file, "r+") do |f|
lines = []
f.each_line do |line|
if (matches = line.scan(/(?<!\s)\.(?<slot>#{Regexp.union(all_registered_slot_names)})/))
next if matches.size == 0

if (matches = line.scan(/(?<!\s)\.(?<slot>#{Regexp.union(all_registered_slot_names)})\b/))
matches.flatten.each do |match|
next if @suggestions.find { |s| s.file == file && s.line == f.lineno }

suggestions << Suggestion.new(file, f.lineno, "maybe replace `.#{match}` with `.with_#{match}`")
message = if @migrate
"replaced `#{match}` with `with_#{match}`"
else
"maybe replace `#{match}` with `with_#{match}`"
end
suggestions << Suggestion.new(file, f.lineno, message)
if @migrate
line.gsub!(/(?<!\s)\.(#{match})\b/, ".with_\\1")
end
end
end
lines << line
end

if @migrate
f.rewind
f.write(lines.join)
end
end
end
Expand Down Expand Up @@ -135,11 +168,11 @@ def all_registered_slot_names
end

def view_component_files
Dir.glob(Rails.root.join(view_component_path_glob, "**", "*.{rb,#{TEMPLATE_LANGUAGES}}"))
Dir.glob(Pathname.new(File.join(view_component_path_glob, "**", "*.{rb,#{TEMPLATE_LANGUAGES}}")))
end

def view_files
Dir.glob(Rails.root.join(view_path_glob, "**", "*.{#{TEMPLATE_LANGUAGES}}"))
Dir.glob(Pathname.new(File.join(view_path_glob, "**", "*.{#{TEMPLATE_LANGUAGES}}")))
end

def all_files
Expand All @@ -159,9 +192,16 @@ def view_component_path_glob
"{#{view_component_paths.join(",")}}"
end

def rails_view_paths
ActionController::Base.view_paths.select do |path|
path.to_s.include?(Rails.root.to_s)
end.map(&:to_s)
end

def view_paths
@view_paths ||= [
"app/views",
rails_view_paths,
Rails.application.config.view_component.preview_paths,
Spone marked this conversation as resolved.
Show resolved Hide resolved
@view_path
].flatten.compact.uniq
end
Expand Down
7 changes: 7 additions & 0 deletions test/sandbox/app/helpers/aliases_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module AliasesHelper
def sandbox_slots(*args, **kwargs, &block)
render SlotsComponent.new(*args, **kwargs), &block
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<%= sandbox_slots do |component| %>
<% component.subtitle do %>
<small>This is my subtitle!</small>
<% end %>
<% end %>
14 changes: 14 additions & 0 deletions test/sandbox/app/views/codemods/_v2_slots_setters_exact.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%= render SlotsComponent.new do |component| %>
<% component.title do %>
This is my title!
<% end %>
<% end %>

<%= render SlotsComponent.new do |component| %>
<% component.tab do %>
<h1>Tab A</h1>
<% end %>
<% component.tab do %>
<h1>Tab B</h1>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
= render(EmptySlotComponent.new) do |component|
- component.title
- component.with_title
- nil
57 changes: 57 additions & 0 deletions test/sandbox/test/codemods/v3_slot_setters_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

require "test_helper"
require "view_component/codemods/v3_slot_setters"

class V3SlotSettersTest < Minitest::Test
def teardown
restore_legacy_slots
end

def test_detects_legacy_slots
output = capture_output do
ViewComponent::Codemods::V3SlotSetters.new.call
end

assert_match "_v2_slots_setters_exact.html.erb\n=> line 2: probably replace `component.title` with `component.with_title`", output
assert_match "line 8: probably replace `component.tab` with `component.with_tab`", output
assert_match "line 11: probably replace `component.tab` with `component.with_tab`", output
assert_match "_v2_slots_setters_alias.html.erb\n=> line 2: maybe replace `subtitle` with `with_subtitle`", output
end

def test_migrate_legacy_slots
ViewComponent::Codemods::V3SlotSetters.new(migrate: true).call

output = capture_output do
ViewComponent::Codemods::V3SlotSetters.new.call
end

refute_match "_v2_slots_setters_exact.html.erb\n=> line 2: probably replace `component.title` with `component.with_title`", output
refute_match "line 6: probably replace `component.tab` with `component.with_tab`", output
refute_match "line 9: probably replace `component.tab` with `component.with_tab`", output
refute_match "_v2_slots_setters_alias.html.erb\n=> line 2: maybe replace `subtitle` with `with_subtitle`", output
end

private

def capture_output
original_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = original_stdout
end

def restore_legacy_slots
test_views = [
Rails.root.join("app/views/codemods/_v2_slots_setters_alias.html.erb"),
Rails.root.join("app/views/codemods/_v2_slots_setters_exact.html.erb")
]
test_views.each do |file|
content = File.read(file)
content.gsub!("with_", "")
File.write(file, content)
end
end
end