Skip to content

Commit

Permalink
PrismScanner: Handles more cases (#621)
Browse files Browse the repository at this point in the history
- Handles more assignments and method calls
  • Loading branch information
davidwessman authored Feb 9, 2025
1 parent ea698da commit 35f12df
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 48 deletions.
4 changes: 3 additions & 1 deletion lib/i18n/tasks/scanners/prism_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def process_prism_parse_result(path, parsed, comments = nil)
next node.occurrences(path) if node.is_a?(I18n::Tasks::Scanners::PrismScanners::TranslationNode)
next unless node.respond_to?(:translation_nodes)

node.translation_nodes.flat_map { |n| n.occurrences(path) }
node.translation_nodes.flat_map do |child|
child.occurrences(path)
end
end
.flatten(1)
end
Expand Down
69 changes: 46 additions & 23 deletions lib/i18n/tasks/scanners/prism_scanners/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,37 +224,40 @@ def translation_nodes(path: nil, options: nil)
translation_nodes_from_calls(path: local_path, options: options)
end

def translation_nodes_from_calls(path: nil, options: nil) # rubocop:disable Metrics/MethodLength
def translation_nodes_from_calls(path: nil, options: nil) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
options ||= {}
other_def_nodes = options[:def_nodes] || []
@calls
.filter_map do |call|
next if call.nil?

case call.type
when :translation_node
call.with_context(path: path, options: options)
when :call_node
other_method =
other_def_nodes&.find do |m|
m.name.to_s == call.name.to_s && m.receiver == call.receiver
end
next if other_method.nil?

other_method.add_call_from(@node.name.to_s)
other_method.translation_nodes(path: path, options: options)
next if call.nil?
next unless call.respond_to?(:type)

case call.type
when :translation_node
call.with_context(path: path, options: options)
when :call_node
other_method =
other_def_nodes&.find do |m|
m.name.to_s == call.name.to_s && m.receiver == call.receiver
end
next if other_method.nil?

other_method.add_call_from(@node.name.to_s)
other_method.translation_nodes(path: path, options: options)
when :local_variable_target_node, :string_node
# Do nothing with these
next
else
if call.respond_to?(:translation_nodes)
call.translation_nodes(path: path, options: options)
else
if call.respond_to?(:translation_nodes)
call.translation_nodes(path: path, options: options)
else
puts(
"Cannot handle calls with: #{call.type},
puts(
"Cannot handle calls with: #{call.type},
if it can contain translations please add it to the case statement."
)
end
)
end
end
.flatten(1)
end.flatten(1)
end

def before_action_translation_nodes(path: nil, options: nil)
Expand All @@ -270,6 +273,26 @@ def before_action_translation_nodes(path: nil, options: nil)
end
end

# We need to keep track of interpolated strings since they can be used as arguments to
# our translation methods, but also contain translations.
class InterpolatedStringNode < BaseNode
def initialize(node:, parts:)
@node = node
@parts = parts
super(node: node)
end

def type
:interpolated_string_node
end

def translation_nodes(path: nil, options: nil)
@parts.filter_map do |part|
part.with_context(path: path, options: options) if part.try(:type) == :translation_node
end.flatten
end
end

class TranslationNode < BaseNode
attr_reader(:key, :node, :options)

Expand Down
15 changes: 0 additions & 15 deletions lib/i18n/tasks/scanners/prism_scanners/rails_visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,6 @@ def visit_call_node(node)
end
end

def handle_translation_call(node, comment_translations)
array_args, keywords = process_arguments(node)
key = array_args.first

receiver = visit(node.receiver) if node.receiver

TranslationNode.new(
node: node,
key: key,
receiver: receiver,
options: keywords,
comment_translations: comment_translations
)
end

def handle_before_action(node) # rubocop:disable Metrics/MethodLength
array_arguments, keywords = process_arguments(node)

Expand Down
48 changes: 40 additions & 8 deletions lib/i18n/tasks/scanners/prism_scanners/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ def prepare_comments_by_line(comments)
end

def visit_statements_node(node)
node.body.map { |child| child.accept(self) }
node.child_nodes.map { |n| visit(n) }.flatten
end

def visit_embedded_statements_node(node)
visit(node.statements)
end

def visit_program_node(node)
node.statements&.body&.map { |child| child.accept(self) }
visit(node.statements)
end

def visit_module_node(node)
Expand All @@ -79,15 +83,23 @@ def visit_class_node(node)
end

def visit_instance_variable_write_node(node)
node.child_nodes.map { |n| visit(n) }
node.child_nodes.map { |n| visit(n) }.flatten
end

def visit_local_variable_write_node(node)
node.child_nodes.map { |n| visit(n) }
node.child_nodes.map { |n| visit(n) }.flatten
end

def visit_local_variable_target_node(node)
node.child_nodes.map { |n| visit(n) }.flatten
end

def visit_multi_write_node(node)
node.child_nodes.map { |n| visit(n) }.flatten
end

def visit_def_node(node)
calls = node.body.child_nodes.filter_map { |n| visit(n) }.flatten
calls = visit(node.body)&.flatten || []

DefNode.new(node: node, calls: calls, private_method: @private_methods)
end
Expand Down Expand Up @@ -139,7 +151,7 @@ def visit_call_node(node)
end

def visit_assoc_node(node)
[visit(node.key), visit(node.value)]
[visit(node.key), visit(node.value)].flatten
end

def visit_symbol_node(node)
Expand All @@ -150,6 +162,11 @@ def visit_string_node(node)
node.content
end

def visit_interpolated_string_node(node)
parts = node.parts.map { |n| visit(n) }.flatten
I18n::Tasks::Scanners::PrismScanners::InterpolatedStringNode.new(node: node, parts: parts)
end

def visit_integer_node(node)
node.value
end
Expand All @@ -170,17 +187,32 @@ def visit_arguments_node(node)
end

def visit_array_node(node)
node.child_nodes.map { |n| visit(n) }
node.elements.map { |n| visit(n) }.flatten
end

def visit_keyword_hash_node(node)
node.elements.to_h { |n| visit(n) }
hash = {}

node.elements.each do |child|
case child.type
when :assoc_node
# Cannot use visit_assoc since it flattens the result
hash[visit(child.key)] = visit(child.value)
else
fail(ArgumentError, "Unexpected node type: #{child.type}")
end
end

hash
end

def handle_translation_call(node, comment_translations)
array_args, keywords = process_arguments(node)
key = array_args.first

# We cannot handle keys that are interpolated strings
return CallNode.new(node: node, comment_translations: comment_translations) if key.is_a?(InterpolatedStringNode)

receiver = visit(node.receiver) if node.receiver

TranslationNode.new(
Expand Down
21 changes: 21 additions & 0 deletions spec/fixtures/prism_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Prism
class PrismController < ApplicationController
class EmptyClass
end

before_action(:authenticate_user!)


def index
end

def show
@user = current_user
%w[testing some keys]
["testing", "keys", t('.relative_key')]
assign, multiple = "sha256=#{t("prism.show.assign")}", "sha256=#{t("prism.show.multiple")}"
end
end
end
33 changes: 32 additions & 1 deletion spec/prism_scanner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ class ApplicationController < ActionController::Base
).to be_empty
end

it 'handles empty method' do
source = <<~RUBY
class EventsController < ApplicationController
def create
end
end
RUBY

expect(
process_string('app/controllers/events_controller.rb', source)
).to be_empty
end

it 'handles more syntax' do
occurrences =
process_path('./spec/fixtures/prism_controller.rb')

expect(occurrences.map(&:first).uniq).to match_array(
%w[
prism.prism.show.relative_key
prism.show.assign
prism.show.multiple
]
)
end

it 'handles before_action as lambda' do
source = <<~RUBY
class EventsController < ApplicationController
Expand Down Expand Up @@ -415,7 +441,12 @@ def method_in_before_action1
'spec/fixtures/used_keys/app/controllers/events_controller.rb'
)
expect(occurrences.map(&:first).uniq).to match_array(
%w[absolute_key events.create.relative_key very_absolute_key events.method_a.from_before_action]
%w[
absolute_key
events.create.relative_key
events.method_a.from_before_action
very_absolute_key
]
)
end
end
Expand Down

0 comments on commit 35f12df

Please sign in to comment.