Skip to content

Commit

Permalink
PrismScanner: Rewrite to avoid implementing visit
Browse files Browse the repository at this point in the history
- This rewrites the Visitor to avoid implementing visit for every
  available node type and instead let the Prism::Visitor visit ALL nodes.
  • Loading branch information
davidwessman committed Mar 2, 2025
1 parent 1b47f72 commit 0bcccee
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 799 deletions.
60 changes: 35 additions & 25 deletions lib/i18n/tasks/scanners/prism_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@

module I18n::Tasks::Scanners
class PrismScanner < FileScanner
MAGIC_COMMENT_SKIP_PRISM = 'i18n-tasks-skip-prism'

def initialize(**args)
unless RAILS_VISITOR || RUBY_VISITOR
unless VISITOR
warn(
'Please make sure `prism` is available to use this feature. Fallback to Ruby AST Scanner.'
)
end
super

@visitor_class = config[:prism_visitor] == 'ruby' ? RUBY_VISITOR : RAILS_VISITOR
@fallback = RubyAstScanner.new(**args)
end

Expand All @@ -23,13 +24,9 @@ def initialize(**args)
#
# @return [Array<[key, Results::KeyOccurrence]>] each occurrence found in the file
def scan_file(path)
return @fallback.send(:scan_file, path) if @visitor_class.nil?
return @fallback.send(:scan_file, path) if VISITOR.nil?

process_prism_parse_result(
path,
PARSER.parse_file(path).value,
PARSER.parse_file_comments(path)
)
process_results(path, PARSER.parse_file(path))
rescue Exception => e # rubocop:disable Lint/RescueException
raise(
::I18n::Tasks::CommandError.new(
Expand All @@ -39,35 +36,48 @@ def scan_file(path)
)
end

def process_prism_parse_result(path, parsed, comments = nil)
return @fallback.send(:scan_file, path) if RUBY_VISITOR.skip_prism_comment?(comments)
# Need to have method tha can be overridden
def process_results(path, parse_results)
parsed = parse_results.value
comments = parse_results.comments

return @fallback.send(:scan_file, path) if skip_prism_comment?(comments)

rails = if config[:prism_visitor].blank?
true
else
config[:prism_visitor] != 'ruby'
end

visitor = @visitor_class.new(comments: comments)
nodes = parsed.accept(visitor)
visitor = VISITOR.new(comments: comments, rails: rails)
parsed.accept(visitor)

nodes
.filter_map do |node|
next node.occurrences(path) if node.is_a?(I18n::Tasks::Scanners::PrismScanners::TranslationNode)
next unless node.respond_to?(:translation_nodes)
occurrences = []
visitor.process.each do |translation_call|
result = translation_call.occurrences(path)
occurrences << result if result
end

occurrences
end

node.translation_nodes.flat_map do |child|
child.occurrences(path)
end
end
.flatten(1)
def skip_prism_comment?(comments)
comments.any? do |comment|
content =
comment.respond_to?(:slice) ? comment.slice : comment.location.slice
content.include?(MAGIC_COMMENT_SKIP_PRISM)
end
end

# This block handles adding a fallback if the `prism` gem is not available.
begin
require 'prism'
require_relative 'prism_scanners/rails_visitor'
require_relative 'prism_scanners/visitor'
PARSER = Prism
RUBY_VISITOR = I18n::Tasks::Scanners::PrismScanners::Visitor
RAILS_VISITOR = I18n::Tasks::Scanners::PrismScanners::RailsVisitor
VISITOR = I18n::Tasks::Scanners::PrismScanners::Visitor
rescue LoadError
PARSER = nil
RUBY_VISITOR, RAILS_VISITOR = nil
VISITOR
end
end
end
41 changes: 41 additions & 0 deletions lib/i18n/tasks/scanners/prism_scanners/arguments_visitor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'prism/visitor'

# This class is used to parse the arguments to e.g. a Prism::CallNode and return the values we need
# for turning them into translations and occurrences.
# Used in the PrismScanners::Visitor class.
module I18n::Tasks::Scanners::PrismScanners
class ArgumentsVisitor < Prism::Visitor
def visit_keyword_hash_node(node)
node.child_nodes.each_with_object({}) do |child, hash|
hash[visit(child.key)] = visit(child.value)
hash
end
end

def visit_symbol_node(node)
node.value
end

def visit_string_node(node)
node.content
end

def visit_array_node(node)
node.child_nodes.map { |child| visit(child) }
end

def visit_arguments_node(node)
node.child_nodes.map { |child| visit(child) }
end

def visit_integer_node(node)
node.value
end

def visit_lambda_node(node)
node
end
end
end
Loading

0 comments on commit 0bcccee

Please sign in to comment.