Warning: file_get_contents(https://raw.githubusercontent.com/Den1xxx/Filemanager/master/languages/ru.json): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 88

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 215

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 216

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 217

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 218

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 219

Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 220
PK!™parser_current.rbnu[# frozen_string_literal: true # :markup: markdown # typed: ignore # module Prism module Translation case RUBY_VERSION when /^3\.3\./ ParserCurrent = Parser33 when /^3\.4\./ ParserCurrent = Parser34 when /^3\.5\./, /^4\.0\./ ParserCurrent = Parser40 when /^4\.1\./ ParserCurrent = Parser41 else # Keep this in sync with released Ruby. parser = Parser40 major, minor, _patch = Gem::Version.new(RUBY_VERSION).segments warn "warning: `Prism::Translation::Current` is loading #{parser.name}, " \ "but you are running #{major}.#{minor}." ParserCurrent = parser end end end PK!XJDDruby_parser.rbnu[# frozen_string_literal: true # :markup: markdown begin require "sexp" rescue LoadError warn(%q{Error: Unable to load sexp. Add `gem "sexp_processor"` to your Gemfile.}) exit(1) end class RubyParser # :nodoc: class SyntaxError < RuntimeError # :nodoc: end end module Prism module Translation # This module is the entry-point for converting a prism syntax tree into the # seattlerb/ruby_parser gem's syntax tree. class RubyParser # A prism visitor that builds Sexp objects. class Compiler < ::Prism::Compiler # This is the name of the file that we are compiling. We set it on every # Sexp object that is generated, and also use it to compile `__FILE__` # nodes. attr_reader :file # Class variables will change their type based on if they are inside of # a method definition or not, so we need to track that state. attr_reader :in_def # Some nodes will change their representation if they are inside of a # pattern, so we need to track that state. attr_reader :in_pattern # Initialize a new compiler with the given file name. def initialize(file, in_def: false, in_pattern: false) @file = file @in_def = in_def @in_pattern = in_pattern end # ``` # alias foo bar # ^^^^^^^^^^^^^ # ``` def visit_alias_method_node(node) s(node, :alias, visit(node.new_name), visit(node.old_name)) end # ``` # alias $foo $bar # ^^^^^^^^^^^^^^^ # ``` def visit_alias_global_variable_node(node) s(node, :valias, node.new_name.name, node.old_name.name) end # ``` # foo => bar | baz # ^^^^^^^^^ # ``` def visit_alternation_pattern_node(node) s(node, :or, visit(node.left), visit(node.right)) end # ``` # a and b # ^^^^^^^ # ``` def visit_and_node(node) left = visit(node.left) if left[0] == :and # ruby_parser has the and keyword as right-associative as opposed to # prism which has it as left-associative. We reverse that # associativity here. nest = left nest = nest[2] while nest[2][0] == :and nest[2] = s(node, :and, nest[2], visit(node.right)) left else s(node, :and, left, visit(node.right)) end end # ``` # [] # ^^ # ``` def visit_array_node(node) if in_pattern s(node, :array_pat, nil).concat(visit_all(node.elements)) else s(node, :array).concat(visit_all(node.elements)) end end # ``` # foo => [bar] # ^^^^^ # ``` def visit_array_pattern_node(node) if node.constant.nil? && node.requireds.empty? && node.rest.nil? && node.posts.empty? s(node, :array_pat) else result = s(node, :array_pat, visit_pattern_constant(node.constant)).concat(visit_all(node.requireds)) case node.rest when SplatNode result << :"*#{node.rest.expression&.name}" when ImplicitRestNode result << :* # This doesn't make any sense at all, but since we're trying to # replicate the behavior directly, we'll copy it. result.line(666) end result.concat(visit_all(node.posts)) end end # ``` # foo(bar) # ^^^ # ``` def visit_arguments_node(node) raise "Cannot visit arguments directly" end # ``` # { a: 1 } # ^^^^ # ``` def visit_assoc_node(node) [visit(node.key), visit(node.value)] end # ``` # def foo(**); bar(**); end # ^^ # # { **foo } # ^^^^^ # ``` def visit_assoc_splat_node(node) if node.value.nil? [s(node, :kwsplat)] else [s(node, :kwsplat, visit(node.value))] end end # ``` # $+ # ^^ # ``` def visit_back_reference_read_node(node) s(node, :back_ref, node.name.to_s.delete_prefix("$").to_sym) end # ``` # begin end # ^^^^^^^^^ # ``` def visit_begin_node(node) result = node.statements.nil? ? s(node, :nil) : visit(node.statements) if !node.rescue_clause.nil? if !node.statements.nil? result = s(node.statements, :rescue, result, visit(node.rescue_clause)) else result = s(node.rescue_clause, :rescue, visit(node.rescue_clause)) end current = node.rescue_clause until (current = current.subsequent).nil? result << visit(current) end end if !node.else_clause&.statements.nil? result << visit(node.else_clause) end if !node.ensure_clause.nil? if !node.statements.nil? || !node.rescue_clause.nil? || !node.else_clause.nil? result = s(node.statements || node.rescue_clause || node.else_clause || node.ensure_clause, :ensure, result, visit(node.ensure_clause)) else result = s(node.ensure_clause, :ensure, visit(node.ensure_clause)) end end result end # ``` # foo(&bar) # ^^^^ # ``` def visit_block_argument_node(node) s(node, :block_pass).tap do |result| result << visit(node.expression) unless node.expression.nil? end end # ``` # foo { |; bar| } # ^^^ # ``` def visit_block_local_variable_node(node) node.name end # A block on a keyword or method call. def visit_block_node(node) s(node, :block_pass, visit(node.expression)) end # ``` # def foo(&bar); end # ^^^^ # ``` def visit_block_parameter_node(node) :"&#{node.name}" end # A block's parameters. def visit_block_parameters_node(node) # If this block parameters has no parameters and is using pipes, then # it inherits its location from its shadow locals, even if they're not # on the same lines as the pipes. shadow_loc = true result = if node.parameters.nil? s(node, :args) else shadow_loc = false visit(node.parameters) end if node.opening == "(" result.line = node.opening_loc.start_line result.line_max = node.closing_loc.end_line shadow_loc = false end if node.locals.any? shadow = s(node, :shadow).concat(visit_all(node.locals)) shadow.line = node.locals.first.location.start_line shadow.line_max = node.locals.last.location.end_line result << shadow if shadow_loc result.line = shadow.line result.line_max = shadow.line_max end end result end # ``` # break # ^^^^^ # # break foo # ^^^^^^^^^ # ``` def visit_break_node(node) if node.arguments.nil? s(node, :break) elsif node.arguments.arguments.length == 1 s(node, :break, visit(node.arguments.arguments.first)) else s(node, :break, s(node.arguments, :array).concat(visit_all(node.arguments.arguments))) end end # ``` # foo # ^^^ # # foo.bar # ^^^^^^^ # # foo.bar() {} # ^^^^^^^^^^^^ # ``` def visit_call_node(node) case node.name when :!~ return s(node, :not, visit(node.copy(name: :"=~"))) when :=~ if node.arguments&.arguments&.length == 1 && node.block.nil? case node.receiver when StringNode return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver)) when RegularExpressionNode, InterpolatedRegularExpressionNode return s(node, :match2, visit(node.receiver), visit(node.arguments.arguments.first)) end case node.arguments.arguments.first when RegularExpressionNode, InterpolatedRegularExpressionNode return s(node, :match3, visit(node.arguments.arguments.first), visit(node.receiver)) end end end type = node.attribute_write? ? :attrasgn : :call type = :"safe_#{type}" if node.safe_navigation? arguments = node.arguments&.arguments || [] write_value = arguments.pop if type == :attrasgn block = node.block if block.is_a?(BlockArgumentNode) arguments << block block = nil end result = s(node, type, visit(node.receiver), node.name).concat(visit_all(arguments)) result << visit_write_value(write_value) unless write_value.nil? visit_block(node, result, block) end # ``` # foo.bar += baz # ^^^^^^^^^^^^^^^ # ``` def visit_call_operator_write_node(node) if op_asgn?(node) s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, node.binary_operator) else s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, node.binary_operator, visit_write_value(node.value)) end end # ``` # foo.bar &&= baz # ^^^^^^^^^^^^^^^ # ``` def visit_call_and_write_node(node) if op_asgn?(node) s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, :"&&") else s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, :"&&", visit_write_value(node.value)) end end # ``` # foo.bar ||= baz # ^^^^^^^^^^^^^^^ # ``` def visit_call_or_write_node(node) if op_asgn?(node) s(node, op_asgn_type(node, :op_asgn), visit(node.receiver), visit_write_value(node.value), node.read_name, :"||") else s(node, op_asgn_type(node, :op_asgn2), visit(node.receiver), node.write_name, :"||", visit_write_value(node.value)) end end # Call nodes with operators following them will either be op_asgn or # op_asgn2 nodes. That is determined by their call operator and their # right-hand side. private def op_asgn?(node) node.call_operator == "::" || (node.value.is_a?(CallNode) && node.value.opening_loc.nil? && !node.value.arguments.nil?) end # Call nodes with operators following them can use &. as an operator, # which changes their type by prefixing "safe_". private def op_asgn_type(node, type) node.safe_navigation? ? :"safe_#{type}" : type end # ``` # foo.bar, = 1 # ^^^^^^^ # ``` def visit_call_target_node(node) s(node, :attrasgn, visit(node.receiver), node.name) end # ``` # foo => bar => baz # ^^^^^^^^^^ # ``` def visit_capture_pattern_node(node) visit(node.target) << visit(node.value) end # ``` # case foo; when bar; end # ^^^^^^^^^^^^^^^^^^^^^^^ # ``` def visit_case_node(node) s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause) end # ``` # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ # ``` def visit_case_match_node(node) s(node, :case, visit(node.predicate)).concat(visit_all(node.conditions)) << visit(node.else_clause) end # ``` # class Foo; end # ^^^^^^^^^^^^^^ # ``` def visit_class_node(node) name = if node.constant_path.is_a?(ConstantReadNode) node.name else visit(node.constant_path) end result = if node.body.nil? s(node, :class, name, visit(node.superclass)) elsif node.body.is_a?(StatementsNode) compiler = copy_compiler(in_def: false) s(node, :class, name, visit(node.superclass)).concat(node.body.body.map { |child| child.accept(compiler) }) else s(node, :class, name, visit(node.superclass), node.body.accept(copy_compiler(in_def: false))) end attach_comments(result, node) result end # ``` # @@foo # ^^^^^ # ``` def visit_class_variable_read_node(node) s(node, :cvar, node.name) end # ``` # @@foo = 1 # ^^^^^^^^^ # # @@foo, @@bar = 1 # ^^^^^ ^^^^^ # ``` def visit_class_variable_write_node(node) s(node, class_variable_write_type, node.name, visit_write_value(node.value)) end # ``` # @@foo += bar # ^^^^^^^^^^^^ # ``` def visit_class_variable_operator_write_node(node) s(node, class_variable_write_type, node.name, s(node, :call, s(node, :cvar, node.name), node.binary_operator, visit_write_value(node.value))) end # ``` # @@foo &&= bar # ^^^^^^^^^^^^^ # ``` def visit_class_variable_and_write_node(node) s(node, :op_asgn_and, s(node, :cvar, node.name), s(node, class_variable_write_type, node.name, visit_write_value(node.value))) end # ``` # @@foo ||= bar # ^^^^^^^^^^^^^ # ``` def visit_class_variable_or_write_node(node) s(node, :op_asgn_or, s(node, :cvar, node.name), s(node, class_variable_write_type, node.name, visit_write_value(node.value))) end # ``` # @@foo, = bar # ^^^^^ # ``` def visit_class_variable_target_node(node) s(node, class_variable_write_type, node.name) end # If a class variable is written within a method definition, it has a # different type than everywhere else. private def class_variable_write_type in_def ? :cvasgn : :cvdecl end # ``` # Foo # ^^^ # ``` def visit_constant_read_node(node) s(node, :const, node.name) end # ``` # Foo = 1 # ^^^^^^^ # # Foo, Bar = 1 # ^^^ ^^^ # ``` def visit_constant_write_node(node) s(node, :cdecl, node.name, visit_write_value(node.value)) end # ``` # Foo += bar # ^^^^^^^^^^^ # ``` def visit_constant_operator_write_node(node) s(node, :cdecl, node.name, s(node, :call, s(node, :const, node.name), node.binary_operator, visit_write_value(node.value))) end # ``` # Foo &&= bar # ^^^^^^^^^^^^ # ``` def visit_constant_and_write_node(node) s(node, :op_asgn_and, s(node, :const, node.name), s(node, :cdecl, node.name, visit(node.value))) end # ``` # Foo ||= bar # ^^^^^^^^^^^^ # ``` def visit_constant_or_write_node(node) s(node, :op_asgn_or, s(node, :const, node.name), s(node, :cdecl, node.name, visit(node.value))) end # ``` # Foo, = bar # ^^^ # ``` def visit_constant_target_node(node) s(node, :cdecl, node.name) end # ``` # Foo::Bar # ^^^^^^^^ # ``` def visit_constant_path_node(node) if node.parent.nil? s(node, :colon3, node.name) else s(node, :colon2, visit(node.parent), node.name) end end # ``` # Foo::Bar = 1 # ^^^^^^^^^^^^ # # Foo::Foo, Bar::Bar = 1 # ^^^^^^^^ ^^^^^^^^ # ``` def visit_constant_path_write_node(node) s(node, :cdecl, visit(node.target), visit_write_value(node.value)) end # ``` # Foo::Bar += baz # ^^^^^^^^^^^^^^^ # ``` def visit_constant_path_operator_write_node(node) s(node, :op_asgn, visit(node.target), node.binary_operator, visit_write_value(node.value)) end # ``` # Foo::Bar &&= baz # ^^^^^^^^^^^^^^^^ # ``` def visit_constant_path_and_write_node(node) s(node, :op_asgn_and, visit(node.target), visit_write_value(node.value)) end # ``` # Foo::Bar ||= baz # ^^^^^^^^^^^^^^^^ # ``` def visit_constant_path_or_write_node(node) s(node, :op_asgn_or, visit(node.target), visit_write_value(node.value)) end # ``` # Foo::Bar, = baz # ^^^^^^^^ # ``` def visit_constant_path_target_node(node) inner = if node.parent.nil? s(node, :colon3, node.name) else s(node, :colon2, visit(node.parent), node.name) end s(node, :const, inner) end # ``` # def foo; end # ^^^^^^^^^^^^ # # def self.foo; end # ^^^^^^^^^^^^^^^^^ # ``` def visit_def_node(node) name = node.name_loc.slice.to_sym result = if node.receiver.nil? s(node, :defn, name) else s(node, :defs, visit(node.receiver), name) end attach_comments(result, node) result.line(node.name_loc.start_line) if node.parameters.nil? result << s(node, :args).line(node.name_loc.start_line) else result << visit(node.parameters) end if node.body.nil? result << s(node, :nil) elsif node.body.is_a?(StatementsNode) compiler = copy_compiler(in_def: true) result.concat(node.body.body.map { |child| child.accept(compiler) }) else result << node.body.accept(copy_compiler(in_def: true)) end end # ``` # defined? a # ^^^^^^^^^^ # # defined?(a) # ^^^^^^^^^^^ # ``` def visit_defined_node(node) s(node, :defined, visit(node.value)) end # ``` # if foo then bar else baz end # ^^^^^^^^^^^^ # ``` def visit_else_node(node) visit(node.statements) end # ``` # "foo #{bar}" # ^^^^^^ # ``` def visit_embedded_statements_node(node) result = s(node, :evstr) result << visit(node.statements) unless node.statements.nil? result end # ``` # "foo #@bar" # ^^^^^ # ``` def visit_embedded_variable_node(node) s(node, :evstr, visit(node.variable)) end # ``` # begin; foo; ensure; bar; end # ^^^^^^^^^^^^ # ``` def visit_ensure_node(node) node.statements.nil? ? s(node, :nil) : visit(node.statements) end # ``` # false # ^^^^^ # ``` def visit_false_node(node) s(node, :false) end # ``` # foo => [*, bar, *] # ^^^^^^^^^^^ # ``` def visit_find_pattern_node(node) s(node, :find_pat, visit_pattern_constant(node.constant), :"*#{node.left.expression&.name}", *visit_all(node.requireds), :"*#{node.right.expression&.name}") end # ``` # if foo .. bar; end # ^^^^^^^^^^ # ``` def visit_flip_flop_node(node) if node.left.is_a?(IntegerNode) && node.right.is_a?(IntegerNode) s(node, :lit, Range.new(node.left.value, node.right.value, node.exclude_end?)) else s(node, node.exclude_end? ? :flip3 : :flip2, visit(node.left), visit(node.right)) end end # ``` # 1.0 # ^^^ # ``` def visit_float_node(node) s(node, :lit, node.value) end # ``` # for foo in bar do end # ^^^^^^^^^^^^^^^^^^^^^ # ``` def visit_for_node(node) s(node, :for, visit(node.collection), visit(node.index), visit(node.statements)) end # ``` # def foo(...); bar(...); end # ^^^ # ``` def visit_forwarding_arguments_node(node) s(node, :forward_args) end # ``` # def foo(...); end # ^^^ # ``` def visit_forwarding_parameter_node(node) s(node, :forward_args) end # ``` # super # ^^^^^ # # super {} # ^^^^^^^^ # ``` def visit_forwarding_super_node(node) visit_block(node, s(node, :zsuper), node.block) end # ``` # $foo # ^^^^ # ``` def visit_global_variable_read_node(node) s(node, :gvar, node.name) end # ``` # $foo = 1 # ^^^^^^^^ # # $foo, $bar = 1 # ^^^^ ^^^^ # ``` def visit_global_variable_write_node(node) s(node, :gasgn, node.name, visit_write_value(node.value)) end # ``` # $foo += bar # ^^^^^^^^^^^ # ``` def visit_global_variable_operator_write_node(node) s(node, :gasgn, node.name, s(node, :call, s(node, :gvar, node.name), node.binary_operator, visit(node.value))) end # ``` # $foo &&= bar # ^^^^^^^^^^^^ # ``` def visit_global_variable_and_write_node(node) s(node, :op_asgn_and, s(node, :gvar, node.name), s(node, :gasgn, node.name, visit_write_value(node.value))) end # ``` # $foo ||= bar # ^^^^^^^^^^^^ # ``` def visit_global_variable_or_write_node(node) s(node, :op_asgn_or, s(node, :gvar, node.name), s(node, :gasgn, node.name, visit_write_value(node.value))) end # ``` # $foo, = bar # ^^^^ # ``` def visit_global_variable_target_node(node) s(node, :gasgn, node.name) end # ``` # {} # ^^ # ``` def visit_hash_node(node) s(node, :hash).concat(node.elements.flat_map { |element| visit(element) }) end # ``` # foo => {} # ^^ # ``` def visit_hash_pattern_node(node) result = s(node, :hash_pat, visit_pattern_constant(node.constant)).concat(node.elements.flat_map { |element| visit(element) }) case node.rest when AssocSplatNode result << s(node.rest, :kwrest, :"**#{node.rest.value&.name}") when NoKeywordsParameterNode result << visit(node.rest) end result end # ``` # if foo then bar end # ^^^^^^^^^^^^^^^^^^^ # # bar if foo # ^^^^^^^^^^ # # foo ? bar : baz # ^^^^^^^^^^^^^^^ # ``` def visit_if_node(node) s(node, :if, visit(node.predicate), visit(node.statements), visit(node.subsequent)) end # 1i def visit_imaginary_node(node) s(node, :lit, node.value) end # ``` # { foo: } # ^^^^ # ``` def visit_implicit_node(node) end # ``` # foo { |bar,| } # ^ # ``` def visit_implicit_rest_node(node) end # ``` # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ # ``` def visit_in_node(node) pattern = if node.pattern.is_a?(ConstantPathNode) s(node.pattern, :const, visit(node.pattern)) else node.pattern.accept(copy_compiler(in_pattern: true)) end s(node, :in, pattern).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body)) end # ``` # foo[bar] += baz # ^^^^^^^^^^^^^^^ # ``` def visit_index_operator_write_node(node) arglist = nil if !node.arguments.nil? || !node.block.nil? arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || [])) arglist << visit(node.block) if !node.block.nil? end s(node, :op_asgn1, visit(node.receiver), arglist, node.binary_operator, visit_write_value(node.value)) end # ``` # foo[bar] &&= baz # ^^^^^^^^^^^^^^^^ # ``` def visit_index_and_write_node(node) arglist = nil if !node.arguments.nil? || !node.block.nil? arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || [])) arglist << visit(node.block) if !node.block.nil? end s(node, :op_asgn1, visit(node.receiver), arglist, :"&&", visit_write_value(node.value)) end # ``` # foo[bar] ||= baz # ^^^^^^^^^^^^^^^^ # ``` def visit_index_or_write_node(node) arglist = nil if !node.arguments.nil? || !node.block.nil? arglist = s(node, :arglist).concat(visit_all(node.arguments&.arguments || [])) arglist << visit(node.block) if !node.block.nil? end s(node, :op_asgn1, visit(node.receiver), arglist, :"||", visit_write_value(node.value)) end # ``` # foo[bar], = 1 # ^^^^^^^^ # ``` def visit_index_target_node(node) arguments = visit_all(node.arguments&.arguments || []) arguments << visit(node.block) unless node.block.nil? s(node, :attrasgn, visit(node.receiver), :[]=).concat(arguments) end # ``` # @foo # ^^^^ # ``` def visit_instance_variable_read_node(node) s(node, :ivar, node.name) end # ``` # @foo = 1 # ^^^^^^^^ # # @foo, @bar = 1 # ^^^^ ^^^^ # ``` def visit_instance_variable_write_node(node) s(node, :iasgn, node.name, visit_write_value(node.value)) end # ``` # @foo += bar # ^^^^^^^^^^^ # ``` def visit_instance_variable_operator_write_node(node) s(node, :iasgn, node.name, s(node, :call, s(node, :ivar, node.name), node.binary_operator, visit_write_value(node.value))) end # ``` # @foo &&= bar # ^^^^^^^^^^^^ # ``` def visit_instance_variable_and_write_node(node) s(node, :op_asgn_and, s(node, :ivar, node.name), s(node, :iasgn, node.name, visit(node.value))) end # ``` # @foo ||= bar # ^^^^^^^^^^^^ # ``` def visit_instance_variable_or_write_node(node) s(node, :op_asgn_or, s(node, :ivar, node.name), s(node, :iasgn, node.name, visit(node.value))) end # ``` # @foo, = bar # ^^^^ # ``` def visit_instance_variable_target_node(node) s(node, :iasgn, node.name) end # ``` # 1 # ^ # ``` def visit_integer_node(node) s(node, :lit, node.value) end # ``` # if /foo #{bar}/ then end # ^^^^^^^^^^^^ # ``` def visit_interpolated_match_last_line_node(node) parts = visit_interpolated_parts(node.parts) regexp = if parts.length == 1 s(node, :lit, Regexp.new(parts.first, node.options)) else s(node, :dregx).concat(parts).tap do |result| options = node.options result << options if options != 0 end end s(node, :match, regexp) end # ``` # /foo #{bar}/ # ^^^^^^^^^^^^ # ``` def visit_interpolated_regular_expression_node(node) parts = visit_interpolated_parts(node.parts) if parts.length == 1 s(node, :lit, Regexp.new(parts.first, node.options)) else s(node, :dregx).concat(parts).tap do |result| options = node.options result << options if options != 0 end end end # ``` # "foo #{bar}" # ^^^^^^^^^^^^ # ``` def visit_interpolated_string_node(node) parts = visit_interpolated_parts(node.parts) parts.length == 1 ? s(node, :str, parts.first) : s(node, :dstr).concat(parts) end # ``` # :"foo #{bar}" # ^^^^^^^^^^^^^ # ``` def visit_interpolated_symbol_node(node) parts = visit_interpolated_parts(node.parts) parts.length == 1 ? s(node, :lit, parts.first.to_sym) : s(node, :dsym).concat(parts) end # ``` # `foo #{bar}` # ^^^^^^^^^^^^ # ``` def visit_interpolated_x_string_node(node) source = node.heredoc? ? node.parts.first : node parts = visit_interpolated_parts(node.parts) parts.length == 1 ? s(source, :xstr, parts.first) : s(source, :dxstr).concat(parts) end # Visit the interpolated content of the string-like node. private def visit_interpolated_parts(parts) visited = [] parts.each do |part| result = visit(part) if result[0] == :evstr && result[1] if result[1][0] == :str visited << result[1] elsif result[1][0] == :dstr visited.concat(result[1][1..-1]) else visited << result end visited << :space elsif result[0] == :dstr if !visited.empty? && part.parts[0].is_a?(StringNode) # If we are in the middle of an implicitly concatenated string, # we should not have a bare string as the first part. In this # case we need to visit just that first part and then we can # push the rest of the parts onto the visited array. result[1] = visit(part.parts[0]) end visited.concat(result[1..-1]) else visited << result end end state = :beginning #: :beginning | :string_content | :interpolated_content results = [] visited.each_with_index do |result, index| case state when :beginning if result.is_a?(String) results << result state = :string_content elsif result.is_a?(Array) && result[0] == :str results << result[1] state = :string_content else results << "" results << result state = :interpolated_content end when :string_content if result == :space # continue elsif result.is_a?(String) results[0] = "#{results[0]}#{result}" elsif result.is_a?(Array) && result[0] == :str results[0] = "#{results[0]}#{result[1]}" else results << result state = :interpolated_content end when :interpolated_content if result == :space # continue elsif visited[index - 1] != :space && result.is_a?(Array) && result[0] == :str && results[-1][0] == :str && (results[-1].line_max == result.line) results[-1][1] = "#{results[-1][1]}#{result[1]}" results[-1].line_max = result.line_max else results << result end end end results end # ``` # -> { it } # ^^ # ``` def visit_it_local_variable_read_node(node) s(node, :call, nil, :it) end # ``` # foo(bar: baz) # ^^^^^^^^ # ``` def visit_keyword_hash_node(node) s(node, :hash).concat(node.elements.flat_map { |element| visit(element) }) end # ``` # def foo(**bar); end # ^^^^^ # # def foo(**); end # ^^ # ``` def visit_keyword_rest_parameter_node(node) :"**#{node.name}" end # -> {} def visit_lambda_node(node) parameters = case node.parameters when nil, ItParametersNode, NumberedParametersNode 0 else visit(node.parameters) end if node.body.nil? s(node, :iter, s(node, :lambda), parameters) else s(node, :iter, s(node, :lambda), parameters, visit(node.body)) end end # ``` # foo # ^^^ # ``` def visit_local_variable_read_node(node) if node.name.match?(/^_\d$/) s(node, :call, nil, node.name) else s(node, :lvar, node.name) end end # ``` # foo = 1 # ^^^^^^^ # # foo, bar = 1 # ^^^ ^^^ # ``` def visit_local_variable_write_node(node) s(node, :lasgn, node.name, visit_write_value(node.value)) end # ``` # foo += bar # ^^^^^^^^^^ # ``` def visit_local_variable_operator_write_node(node) s(node, :lasgn, node.name, s(node, :call, s(node, :lvar, node.name), node.binary_operator, visit_write_value(node.value))) end # ``` # foo &&= bar # ^^^^^^^^^^^ # ``` def visit_local_variable_and_write_node(node) s(node, :op_asgn_and, s(node, :lvar, node.name), s(node, :lasgn, node.name, visit_write_value(node.value))) end # ``` # foo ||= bar # ^^^^^^^^^^^ # ``` def visit_local_variable_or_write_node(node) s(node, :op_asgn_or, s(node, :lvar, node.name), s(node, :lasgn, node.name, visit_write_value(node.value))) end # ``` # foo, = bar # ^^^ # ``` def visit_local_variable_target_node(node) s(node, :lasgn, node.name) end # ``` # if /foo/ then end # ^^^^^ # ``` def visit_match_last_line_node(node) s(node, :match, s(node, :lit, Regexp.new(node.unescaped, node.options))) end # ``` # foo in bar # ^^^^^^^^^^ # ``` def visit_match_predicate_node(node) s(node, :case, visit(node.value), s(node, :in, node.pattern.accept(copy_compiler(in_pattern: true)), nil), nil) end # ``` # foo => bar # ^^^^^^^^^^ # ``` def visit_match_required_node(node) s(node, :case, visit(node.value), s(node, :in, node.pattern.accept(copy_compiler(in_pattern: true)), nil), nil) end # ``` # /(?foo)/ =~ bar # ^^^^^^^^^^^^^^^^^^^^ # ``` def visit_match_write_node(node) s(node, :match2, visit(node.call.receiver), visit(node.call.arguments.arguments.first)) end # A node that is missing from the syntax tree. This is only used in the # case of a syntax error. The parser gem doesn't have such a concept, so # we invent our own here. def visit_missing_node(node) raise "Cannot visit missing node directly" end # ``` # module Foo; end # ^^^^^^^^^^^^^^^ # ``` def visit_module_node(node) name = if node.constant_path.is_a?(ConstantReadNode) node.name else visit(node.constant_path) end result = if node.body.nil? s(node, :module, name) elsif node.body.is_a?(StatementsNode) compiler = copy_compiler(in_def: false) s(node, :module, name).concat(node.body.body.map { |child| child.accept(compiler) }) else s(node, :module, name, node.body.accept(copy_compiler(in_def: false))) end attach_comments(result, node) result end # ``` # foo, bar = baz # ^^^^^^^^ # ``` def visit_multi_target_node(node) targets = [*node.lefts] targets << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) targets.concat(node.rights) s(node, :masgn, s(node, :array).concat(visit_all(targets))) end # ``` # foo, bar = baz # ^^^^^^^^^^^^^^ # ``` def visit_multi_write_node(node) targets = [*node.lefts] targets << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) targets.concat(node.rights) value = if node.value.is_a?(ArrayNode) && node.value.opening_loc.nil? if node.value.elements.length == 1 && node.value.elements.first.is_a?(SplatNode) visit(node.value.elements.first) else visit(node.value) end else s(node.value, :to_ary, visit(node.value)) end s(node, :masgn, s(node, :array).concat(visit_all(targets)), value) end # ``` # next # ^^^^ # # next foo # ^^^^^^^^ # ``` def visit_next_node(node) if node.arguments.nil? s(node, :next) elsif node.arguments.arguments.length == 1 argument = node.arguments.arguments.first s(node, :next, argument.is_a?(SplatNode) ? s(node, :svalue, visit(argument)) : visit(argument)) else s(node, :next, s(node, :array).concat(visit_all(node.arguments.arguments))) end end # ``` # nil # ^^^ # ``` def visit_nil_node(node) s(node, :nil) end # ``` # def foo(**nil); end # ^^^^^ # ``` def visit_no_keywords_parameter_node(node) in_pattern ? s(node, :kwrest, :"**nil") : :"**nil" end # ``` # -> { _1 + _2 } # ^^^^^^^^^^^^^^ # ``` def visit_numbered_parameters_node(node) raise "Cannot visit numbered parameters directly" end # ``` # $1 # ^^ # ``` def visit_numbered_reference_read_node(node) s(node, :nth_ref, node.number) end # ``` # def foo(bar: baz); end # ^^^^^^^^ # ``` def visit_optional_keyword_parameter_node(node) s(node, :kwarg, node.name, visit(node.value)) end # ``` # def foo(bar = 1); end # ^^^^^^^ # ``` def visit_optional_parameter_node(node) s(node, :lasgn, node.name, visit(node.value)) end # ``` # a or b # ^^^^^^ # ``` def visit_or_node(node) left = visit(node.left) if left[0] == :or # ruby_parser has the or keyword as right-associative as opposed to # prism which has it as left-associative. We reverse that # associativity here. nest = left nest = nest[2] while nest[2][0] == :or nest[2] = s(node, :or, nest[2], visit(node.right)) left else s(node, :or, left, visit(node.right)) end end # ``` # def foo(bar, *baz); end # ^^^^^^^^^ # ``` def visit_parameters_node(node) children = node.each_child_node.map do |element| if element.is_a?(MultiTargetNode) visit_destructured_parameter(element) else visit(element) end end s(node, :args).concat(children) end # ``` # def foo((bar, baz)); end # ^^^^^^^^^^ # ``` private def visit_destructured_parameter(node) children = [*node.lefts, *node.rest, *node.rights].map do |child| case child when RequiredParameterNode visit(child) when MultiTargetNode visit_destructured_parameter(child) when SplatNode :"*#{child.expression&.name}" else raise end end s(node, :masgn).concat(children) end # ``` # () # ^^ # # (1) # ^^^ # ``` def visit_parentheses_node(node) if node.body.nil? s(node, :nil) else visit(node.body) end end # ``` # foo => ^(bar) # ^^^^^^ # ``` def visit_pinned_expression_node(node) node.expression.accept(copy_compiler(in_pattern: false)) end # ``` # foo = 1 and bar => ^foo # ^^^^ # ``` def visit_pinned_variable_node(node) if node.variable.is_a?(LocalVariableReadNode) && node.variable.name.match?(/^_\d$/) s(node, :lvar, node.variable.name) else visit(node.variable) end end # END {} def visit_post_execution_node(node) s(node, :iter, s(node, :postexe), 0, visit(node.statements)) end # BEGIN {} def visit_pre_execution_node(node) s(node, :iter, s(node, :preexe), 0, visit(node.statements)) end # The top-level program node. def visit_program_node(node) visit(node.statements) end # ``` # 0..5 # ^^^^ # ``` def visit_range_node(node) if !in_pattern && !node.left.nil? && !node.right.nil? && ([node.left.type, node.right.type] - %i[nil_node integer_node]).empty? left = node.left.value if node.left.is_a?(IntegerNode) right = node.right.value if node.right.is_a?(IntegerNode) s(node, :lit, Range.new(left, right, node.exclude_end?)) else s(node, node.exclude_end? ? :dot3 : :dot2, visit_range_bounds_node(node.left), visit_range_bounds_node(node.right)) end end # If the bounds of a range node are empty parentheses, then they do not # get replaced by their usual s(:nil), but instead are s(:begin). private def visit_range_bounds_node(node) if node.is_a?(ParenthesesNode) && node.body.nil? s(node, :begin) else visit(node) end end # ``` # 1r # ^^ # ``` def visit_rational_node(node) s(node, :lit, node.value) end # ``` # redo # ^^^^ # ``` def visit_redo_node(node) s(node, :redo) end # ``` # /foo/ # ^^^^^ # ``` def visit_regular_expression_node(node) s(node, :lit, Regexp.new(node.unescaped, node.options)) end # ``` # def foo(bar:); end # ^^^^ # ``` def visit_required_keyword_parameter_node(node) s(node, :kwarg, node.name) end # ``` # def foo(bar); end # ^^^ # ``` def visit_required_parameter_node(node) node.name end # ``` # foo rescue bar # ^^^^^^^^^^^^^^ # ``` def visit_rescue_modifier_node(node) s(node, :rescue, visit(node.expression), s(node.rescue_expression, :resbody, s(node.rescue_expression, :array), visit(node.rescue_expression))) end # ``` # begin; rescue; end # ^^^^^^^ # ``` def visit_rescue_node(node) exceptions = if node.exceptions.length == 1 && node.exceptions.first.is_a?(SplatNode) visit(node.exceptions.first) else s(node, :array).concat(visit_all(node.exceptions)) end if !node.reference.nil? exceptions << (visit(node.reference) << s(node.reference, :gvar, :"$!")) end s(node, :resbody, exceptions).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body)) end # ``` # def foo(*bar); end # ^^^^ # # def foo(*); end # ^ # ``` def visit_rest_parameter_node(node) :"*#{node.name}" end # ``` # retry # ^^^^^ # ``` def visit_retry_node(node) s(node, :retry) end # ``` # return # ^^^^^^ # # return 1 # ^^^^^^^^ # ``` def visit_return_node(node) if node.arguments.nil? s(node, :return) elsif node.arguments.arguments.length == 1 argument = node.arguments.arguments.first s(node, :return, argument.is_a?(SplatNode) ? s(node, :svalue, visit(argument)) : visit(argument)) else s(node, :return, s(node, :array).concat(visit_all(node.arguments.arguments))) end end # ``` # self # ^^^^ # ``` def visit_self_node(node) s(node, :self) end # A shareable constant. def visit_shareable_constant_node(node) visit(node.write) end # ``` # class << self; end # ^^^^^^^^^^^^^^^^^^ # ``` def visit_singleton_class_node(node) s(node, :sclass, visit(node.expression)).tap do |sexp| sexp << node.body.accept(copy_compiler(in_def: false)) unless node.body.nil? end end # ``` # __ENCODING__ # ^^^^^^^^^^^^ # ``` def visit_source_encoding_node(node) # TODO s(node, :colon2, s(node, :const, :Encoding), :UTF_8) end # ``` # __FILE__ # ^^^^^^^^ # ``` def visit_source_file_node(node) s(node, :str, node.filepath) end # ``` # __LINE__ # ^^^^^^^^ # ``` def visit_source_line_node(node) s(node, :lit, node.location.start_line) end # ``` # foo(*bar) # ^^^^ # # def foo((bar, *baz)); end # ^^^^ # # def foo(*); bar(*); end # ^ # ``` def visit_splat_node(node) if node.expression.nil? s(node, :splat) else s(node, :splat, visit(node.expression)) end end # A list of statements. def visit_statements_node(node) first, *rest = node.body if rest.empty? visit(first) else s(node, :block).concat(visit_all(node.body)) end end # ``` # "foo" # ^^^^^ # ``` def visit_string_node(node) unescaped = node.unescaped if node.forced_binary_encoding? unescaped = unescaped.dup unescaped.force_encoding(Encoding::BINARY) end s(node, :str, unescaped) end # ``` # super(foo) # ^^^^^^^^^^ # ``` def visit_super_node(node) arguments = node.arguments&.arguments || [] block = node.block if block.is_a?(BlockArgumentNode) arguments << block block = nil end visit_block(node, s(node, :super).concat(visit_all(arguments)), block) end # ``` # :foo # ^^^^ # ``` def visit_symbol_node(node) node.value == "!@" ? s(node, :lit, :"!@") : s(node, :lit, node.unescaped.to_sym) end # ``` # true # ^^^^ # ``` def visit_true_node(node) s(node, :true) end # ``` # undef foo # ^^^^^^^^^ # ``` def visit_undef_node(node) names = node.names.map { |name| s(node, :undef, visit(name)) } names.length == 1 ? names.first : s(node, :block).concat(names) end # ``` # unless foo; bar end # ^^^^^^^^^^^^^^^^^^^ # # bar unless foo # ^^^^^^^^^^^^^^ # ``` def visit_unless_node(node) s(node, :if, visit(node.predicate), visit(node.else_clause), visit(node.statements)) end # ``` # until foo; bar end # ^^^^^^^^^^^^^^^^^ # # bar until foo # ^^^^^^^^^^^^^ # ``` def visit_until_node(node) s(node, :until, visit(node.predicate), visit(node.statements), !node.begin_modifier?) end # ``` # case foo; when bar; end # ^^^^^^^^^^^^^ # ``` def visit_when_node(node) s(node, :when, s(node, :array).concat(visit_all(node.conditions))).concat(node.statements.nil? ? [nil] : visit_all(node.statements.body)) end # ``` # while foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar while foo # ^^^^^^^^^^^^^ # ``` def visit_while_node(node) s(node, :while, visit(node.predicate), visit(node.statements), !node.begin_modifier?) end # ``` # `foo` # ^^^^^ # ``` def visit_x_string_node(node) result = s(node, :xstr, node.unescaped) if node.heredoc? result.line = node.content_loc.start_line result.line_max = node.content_loc.end_line end result end # ``` # yield # ^^^^^ # # yield 1 # ^^^^^^^ # ``` def visit_yield_node(node) s(node, :yield).concat(visit_all(node.arguments&.arguments || [])) end private # Attach prism comments to the given sexp. def attach_comments(sexp, node) return unless node.comments return if node.comments.empty? extra = node.location.start_line - node.comments.last.location.start_line comments = node.comments.map(&:slice) comments.concat([nil] * [0, extra].max) sexp.comments = comments.join("\n") end # Create a new compiler with the given options. def copy_compiler(in_def: self.in_def, in_pattern: self.in_pattern) Compiler.new(file, in_def: in_def, in_pattern: in_pattern) end # Create a new Sexp object from the given prism node and arguments. def s(node, *arguments) result = Sexp.new(*arguments) result.file = file result.line = node.location.start_line result.line_max = node.location.end_line result end # Visit a block node, which will modify the AST by wrapping the given # visited node in an iter node. def visit_block(node, sexp, block) if block.nil? sexp else parameters = case block.parameters when nil, ItParametersNode, NumberedParametersNode 0 else visit(block.parameters) end if block.body.nil? s(node, :iter, sexp, parameters) else s(node, :iter, sexp, parameters, visit(block.body)) end end end # Pattern constants get wrapped in another layer of :const. def visit_pattern_constant(node) case node when nil # nothing when ConstantReadNode visit(node) else s(node, :const, visit(node)) end end # Visit the value of a write, which will be on the right-hand side of # a write operator. Because implicit arrays can have splats, those could # potentially be wrapped in an svalue node. def visit_write_value(node) if node.is_a?(ArrayNode) && node.opening_loc.nil? if node.elements.length == 1 && node.elements.first.is_a?(SplatNode) s(node, :svalue, visit(node.elements.first)) else s(node, :svalue, visit(node)) end else visit(node) end end end private_constant :Compiler # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse(source, filepath = "(string)") translate(Prism.parse(source, filepath: filepath, partial_script: true), filepath) end # Parse the given file and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse_file(filepath) translate(Prism.parse_file(filepath, partial_script: true), filepath) end # Parse the give file and translate it into the # seattlerb/ruby_parser gem's Sexp format. This method is # provided for API compatibility to RubyParser and takes an # optional +timeout+ argument. def process(ruby, file = "(string)", timeout = nil) Timeout.timeout(timeout) { parse(ruby, file) } end class << self # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse(source, filepath = "(string)") new.parse(source, filepath) end # Parse the given file and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse_file(filepath) new.parse_file(filepath) end end private # Translate the given parse result and filepath into the # seattlerb/ruby_parser gem's Sexp format. def translate(result, filepath) if result.failure? error = result.errors.first raise ::RubyParser::SyntaxError, "#{filepath}:#{error.location.start_line} :: #{error.message}" end result.attach_comments! result.value.accept(Compiler.new(filepath)) end end end end PK! jC C ripper/sexp.rbnu[# frozen_string_literal: true # :markup: markdown require_relative "../ripper" module Prism module Translation class Ripper # This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that # returns the arrays of [type, *children]. class SexpBuilder < Ripper # :stopdoc: attr_reader :error private def dedent_element(e, width) if (n = dedent_string(e[1], width)) > 0 e[2][1] += n end e end def on_heredoc_dedent(val, width) sub = proc do |cont| cont.map! do |e| if Array === e case e[0] when :@tstring_content e = dedent_element(e, width) when /_add\z/ e[1] = sub[e[1]] end elsif String === e dedent_string(e, width) end e end end sub[val] val end events = private_instance_methods(false).grep(/\Aon_/) {$'.to_sym} (PARSER_EVENTS - events).each do |event| module_eval(<<-End, __FILE__, __LINE__ + 1) def on_#{event}(*args) args.unshift :#{event} end End end SCANNER_EVENTS.each do |event| module_eval(<<-End, __FILE__, __LINE__ + 1) def on_#{event}(tok) [:@#{event}, tok, [lineno(), column()]] end End end def on_error(mesg) @error = mesg end remove_method :on_parse_error alias on_parse_error on_error alias compile_error on_error # :startdoc: end # This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that # returns the same values as ::Ripper::SexpBuilder except with a couple of # niceties that flatten linked lists into arrays. class SexpBuilderPP < SexpBuilder # :stopdoc: private def on_heredoc_dedent(val, width) val.map! do |e| next e if Symbol === e and /_content\z/ =~ e if Array === e and e[0] == :@tstring_content e = dedent_element(e, width) elsif String === e dedent_string(e, width) end e end val end def _dispatch_event_new [] end def _dispatch_event_push(list, item) list.push item list end def on_mlhs_paren(list) [:mlhs, *list] end def on_mlhs_add_star(list, star) list.push([:rest_param, star]) end def on_mlhs_add_post(list, post) list.concat(post) end PARSER_EVENT_TABLE.each do |event, arity| if /_new\z/ =~ event and arity == 0 alias_method "on_#{event}", :_dispatch_event_new elsif /_add\z/ =~ event alias_method "on_#{event}", :_dispatch_event_push end end # :startdoc: end end end end PK!,>0  ripper/lexer.rbnu[# frozen_string_literal: true # :markup: markdown require_relative "../ripper" module Prism module Translation class Ripper class Lexer # :nodoc: # :stopdoc: class State attr_reader :to_int, :to_s def initialize(i) @to_int = i @to_s = Ripper.lex_state_name(i) freeze end def [](index) case index when 0, :to_int @to_int when 1, :to_s @to_s else nil end end alias to_i to_int alias inspect to_s def pretty_print(q) q.text(to_s) end def ==(i) super or to_int == i end def &(i) self.class.new(to_int & i) end def |(i) self.class.new(to_int | i) end def allbits?(i) to_int.allbits?(i) end def anybits?(i) to_int.anybits?(i) end def nobits?(i) to_int.nobits?(i) end end # :startdoc: end end end end PK!!ۘripper/shim.rbnu[# frozen_string_literal: true # This writes the prism ripper translation into the Ripper constant so that # users can transparently use Ripper without any changes. Ripper = Prism::Translation::Ripper PK!n~!!parser_versions.rbnu[# frozen_string_literal: true # :markup: markdown module Prism module Translation # This class is the entry-point for Ruby 3.3 of `Prism::Translation::Parser`. class Parser33 < Parser def version # :nodoc: 33 end end # This class is the entry-point for Ruby 3.4 of `Prism::Translation::Parser`. class Parser34 < Parser def version # :nodoc: 34 end end # This class is the entry-point for Ruby 4.0 of `Prism::Translation::Parser`. class Parser40 < Parser def version # :nodoc: 40 end end Parser35 = Parser40 # :nodoc: # This class is the entry-point for Ruby 4.1 of `Prism::Translation::Parser`. class Parser41 < Parser def version # :nodoc: 41 end end end end PK!hparser/lexer.rbnu[# frozen_string_literal: true # :markup: markdown require "strscan" require_relative "../../polyfill/append_as_bytes" require_relative "../../polyfill/scan_byte" module Prism module Translation class Parser # Accepts a list of prism tokens and converts them into the expected # format for the parser gem. class Lexer # These tokens are always skipped TYPES_ALWAYS_SKIP = Set.new(%i[IGNORED_NEWLINE __END__ EOF]) private_constant :TYPES_ALWAYS_SKIP # The direct translating of types between the two lexers. TYPES = { # These tokens should never appear in the output of the lexer. MISSING: nil, NOT_PROVIDED: nil, EMBDOC_END: nil, EMBDOC_LINE: nil, # These tokens have more or less direct mappings. AMPERSAND: :tAMPER2, AMPERSAND_AMPERSAND: :tANDOP, AMPERSAND_AMPERSAND_EQUAL: :tOP_ASGN, AMPERSAND_DOT: :tANDDOT, AMPERSAND_EQUAL: :tOP_ASGN, BACK_REFERENCE: :tBACK_REF, BACKTICK: :tXSTRING_BEG, BANG: :tBANG, BANG_EQUAL: :tNEQ, BANG_TILDE: :tNMATCH, BRACE_LEFT: :tLCURLY, BRACE_RIGHT: :tRCURLY, BRACKET_LEFT: :tLBRACK2, BRACKET_LEFT_ARRAY: :tLBRACK, BRACKET_LEFT_RIGHT: :tAREF, BRACKET_LEFT_RIGHT_EQUAL: :tASET, BRACKET_RIGHT: :tRBRACK, CARET: :tCARET, CARET_EQUAL: :tOP_ASGN, CHARACTER_LITERAL: :tCHARACTER, CLASS_VARIABLE: :tCVAR, COLON: :tCOLON, COLON_COLON: :tCOLON2, COMMA: :tCOMMA, COMMENT: :tCOMMENT, CONSTANT: :tCONSTANT, DOT: :tDOT, DOT_DOT: :tDOT2, DOT_DOT_DOT: :tDOT3, EMBDOC_BEGIN: :tCOMMENT, EMBEXPR_BEGIN: :tSTRING_DBEG, EMBEXPR_END: :tSTRING_DEND, EMBVAR: :tSTRING_DVAR, EQUAL: :tEQL, EQUAL_EQUAL: :tEQ, EQUAL_EQUAL_EQUAL: :tEQQ, EQUAL_GREATER: :tASSOC, EQUAL_TILDE: :tMATCH, FLOAT: :tFLOAT, FLOAT_IMAGINARY: :tIMAGINARY, FLOAT_RATIONAL: :tRATIONAL, FLOAT_RATIONAL_IMAGINARY: :tIMAGINARY, GLOBAL_VARIABLE: :tGVAR, GREATER: :tGT, GREATER_EQUAL: :tGEQ, GREATER_GREATER: :tRSHFT, GREATER_GREATER_EQUAL: :tOP_ASGN, HEREDOC_START: :tSTRING_BEG, HEREDOC_END: :tSTRING_END, IDENTIFIER: :tIDENTIFIER, INSTANCE_VARIABLE: :tIVAR, INTEGER: :tINTEGER, INTEGER_IMAGINARY: :tIMAGINARY, INTEGER_RATIONAL: :tRATIONAL, INTEGER_RATIONAL_IMAGINARY: :tIMAGINARY, KEYWORD_ALIAS: :kALIAS, KEYWORD_AND: :kAND, KEYWORD_BEGIN: :kBEGIN, KEYWORD_BEGIN_UPCASE: :klBEGIN, KEYWORD_BREAK: :kBREAK, KEYWORD_CASE: :kCASE, KEYWORD_CLASS: :kCLASS, KEYWORD_DEF: :kDEF, KEYWORD_DEFINED: :kDEFINED, KEYWORD_DO: :kDO, KEYWORD_DO_LOOP: :kDO_COND, KEYWORD_END: :kEND, KEYWORD_END_UPCASE: :klEND, KEYWORD_ENSURE: :kENSURE, KEYWORD_ELSE: :kELSE, KEYWORD_ELSIF: :kELSIF, KEYWORD_FALSE: :kFALSE, KEYWORD_FOR: :kFOR, KEYWORD_IF: :kIF, KEYWORD_IF_MODIFIER: :kIF_MOD, KEYWORD_IN: :kIN, KEYWORD_MODULE: :kMODULE, KEYWORD_NEXT: :kNEXT, KEYWORD_NIL: :kNIL, KEYWORD_NOT: :kNOT, KEYWORD_OR: :kOR, KEYWORD_REDO: :kREDO, KEYWORD_RESCUE: :kRESCUE, KEYWORD_RESCUE_MODIFIER: :kRESCUE_MOD, KEYWORD_RETRY: :kRETRY, KEYWORD_RETURN: :kRETURN, KEYWORD_SELF: :kSELF, KEYWORD_SUPER: :kSUPER, KEYWORD_THEN: :kTHEN, KEYWORD_TRUE: :kTRUE, KEYWORD_UNDEF: :kUNDEF, KEYWORD_UNLESS: :kUNLESS, KEYWORD_UNLESS_MODIFIER: :kUNLESS_MOD, KEYWORD_UNTIL: :kUNTIL, KEYWORD_UNTIL_MODIFIER: :kUNTIL_MOD, KEYWORD_WHEN: :kWHEN, KEYWORD_WHILE: :kWHILE, KEYWORD_WHILE_MODIFIER: :kWHILE_MOD, KEYWORD_YIELD: :kYIELD, KEYWORD___ENCODING__: :k__ENCODING__, KEYWORD___FILE__: :k__FILE__, KEYWORD___LINE__: :k__LINE__, LABEL: :tLABEL, LABEL_END: :tLABEL_END, LAMBDA_BEGIN: :tLAMBEG, LESS: :tLT, LESS_EQUAL: :tLEQ, LESS_EQUAL_GREATER: :tCMP, LESS_LESS: :tLSHFT, LESS_LESS_EQUAL: :tOP_ASGN, METHOD_NAME: :tFID, MINUS: :tMINUS, MINUS_EQUAL: :tOP_ASGN, MINUS_GREATER: :tLAMBDA, NEWLINE: :tNL, NUMBERED_REFERENCE: :tNTH_REF, PARENTHESIS_LEFT: :tLPAREN2, PARENTHESIS_LEFT_PARENTHESES: :tLPAREN_ARG, PARENTHESIS_RIGHT: :tRPAREN, PERCENT: :tPERCENT, PERCENT_EQUAL: :tOP_ASGN, PERCENT_LOWER_I: :tQSYMBOLS_BEG, PERCENT_LOWER_W: :tQWORDS_BEG, PERCENT_UPPER_I: :tSYMBOLS_BEG, PERCENT_UPPER_W: :tWORDS_BEG, PERCENT_LOWER_X: :tXSTRING_BEG, PLUS: :tPLUS, PLUS_EQUAL: :tOP_ASGN, PIPE_EQUAL: :tOP_ASGN, PIPE: :tPIPE, PIPE_PIPE: :tOROP, PIPE_PIPE_EQUAL: :tOP_ASGN, QUESTION_MARK: :tEH, REGEXP_BEGIN: :tREGEXP_BEG, REGEXP_END: :tSTRING_END, SEMICOLON: :tSEMI, SLASH: :tDIVIDE, SLASH_EQUAL: :tOP_ASGN, STAR: :tSTAR2, STAR_EQUAL: :tOP_ASGN, STAR_STAR: :tPOW, STAR_STAR_EQUAL: :tOP_ASGN, STRING_BEGIN: :tSTRING_BEG, STRING_CONTENT: :tSTRING_CONTENT, STRING_END: :tSTRING_END, SYMBOL_BEGIN: :tSYMBEG, TILDE: :tTILDE, UAMPERSAND: :tAMPER, UCOLON_COLON: :tCOLON3, UDOT_DOT: :tBDOT2, UDOT_DOT_DOT: :tBDOT3, UMINUS: :tUMINUS, UMINUS_NUM: :tUNARY_NUM, UPLUS: :tUPLUS, USTAR: :tSTAR, USTAR_STAR: :tDSTAR, WORDS_SEP: :tSPACE } # These constants represent flags in our lex state. We really, really # don't want to be using them and we really, really don't want to be # exposing them as part of our public API. Unfortunately, we don't have # another way of matching the exact tokens that the parser gem expects # without them. We should find another way to do this, but in the # meantime we'll hide them from the documentation and mark them as # private constants. EXPR_BEG = 0x1 # :nodoc: EXPR_LABEL = 0x400 # :nodoc: # It is used to determine whether `do` is of the token type `kDO` or `kDO_LAMBDA`. # # NOTE: In edge cases like `-> (foo = -> (bar) {}) do end`, please note that `kDO` is still returned # instead of `kDO_LAMBDA`, which is expected: https://github.com/ruby/prism/pull/3046 LAMBDA_TOKEN_TYPES = Set.new([:kDO_LAMBDA, :tLAMBDA, :tLAMBEG]) # The `PARENTHESIS_LEFT` token in Prism is classified as either `tLPAREN` or `tLPAREN2` in the Parser gem. # The following token types are listed as those classified as `tLPAREN`. LPAREN_CONVERSION_TOKEN_TYPES = Set.new([ :kBREAK, :tCARET, :kCASE, :tDIVIDE, :kFOR, :kIF, :kNEXT, :kRETURN, :kUNTIL, :kWHILE, :tAMPER, :tANDOP, :tBANG, :tCOMMA, :tDOT2, :tDOT3, :tEQL, :tLPAREN, :tLPAREN2, :tLPAREN_ARG, :tLSHFT, :tNL, :tOP_ASGN, :tOROP, :tPIPE, :tSEMI, :tSTRING_DBEG, :tUMINUS, :tUPLUS, :tLCURLY ]) # Types of tokens that are allowed to continue a method call with comments in-between. # For these, the parser gem doesn't emit a newline token after the last comment. COMMENT_CONTINUATION_TYPES = Set.new([:COMMENT, :AMPERSAND_DOT, :DOT]) private_constant :COMMENT_CONTINUATION_TYPES # Heredocs are complex and require us to keep track of a bit of info to refer to later HeredocData = Struct.new(:identifier, :common_whitespace, keyword_init: true) private_constant :TYPES, :EXPR_BEG, :EXPR_LABEL, :LAMBDA_TOKEN_TYPES, :LPAREN_CONVERSION_TOKEN_TYPES, :HeredocData # The Parser::Source::Buffer that the tokens were lexed from. attr_reader :source_buffer # An array of tuples that contain prism tokens and their associated lex # state when they were lexed. attr_reader :lexed # A hash that maps offsets in bytes to offsets in characters. attr_reader :offset_cache # Initialize the lexer with the given source buffer, prism tokens, and # offset cache. def initialize(source_buffer, lexed, offset_cache) @source_buffer = source_buffer @lexed = lexed @offset_cache = offset_cache end Range = ::Parser::Source::Range # :nodoc: private_constant :Range # Convert the prism tokens into the expected format for the parser gem. def to_a tokens = [] index = 0 length = lexed.length heredoc_stack = [] quote_stack = [] # The parser gem emits the newline tokens for comments out of order. This saves # that token location to emit at a later time to properly line everything up. # https://github.com/whitequark/parser/issues/1025 comment_newline_location = nil while index < length token, state = lexed[index] index += 1 next if TYPES_ALWAYS_SKIP.include?(token.type) type = TYPES.fetch(token.type) value = token.value location = range(token.location.start_offset, token.location.end_offset) case type when :kDO nearest_lambda_token = tokens.reverse_each.find do |token| LAMBDA_TOKEN_TYPES.include?(token.first) end if nearest_lambda_token&.first == :tLAMBDA type = :kDO_LAMBDA end when :tCHARACTER value.delete_prefix!("?") # Character literals behave similar to double-quoted strings. We can use the same escaping mechanism. value = unescape_string(value, "?") when :tCOMMENT if token.type == :EMBDOC_BEGIN while !((next_token = lexed[index]&.first) && next_token.type == :EMBDOC_END) && (index < length - 1) value += next_token.value index += 1 end value += next_token.value location = range(token.location.start_offset, next_token.location.end_offset) index += 1 else is_at_eol = value.chomp!.nil? location = range(token.location.start_offset, token.location.end_offset + (is_at_eol ? 0 : -1)) prev_token, _ = lexed[index - 2] if index - 2 >= 0 next_token, _ = lexed[index] is_inline_comment = prev_token&.location&.start_line == token.location.start_line if is_inline_comment && !is_at_eol && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type) tokens << [:tCOMMENT, [value, location]] nl_location = range(token.location.end_offset - 1, token.location.end_offset) tokens << [:tNL, [nil, nl_location]] next elsif is_inline_comment && next_token&.type == :COMMENT comment_newline_location = range(token.location.end_offset - 1, token.location.end_offset) elsif comment_newline_location && !COMMENT_CONTINUATION_TYPES.include?(next_token&.type) tokens << [:tCOMMENT, [value, location]] tokens << [:tNL, [nil, comment_newline_location]] comment_newline_location = nil next end end when :tNL next_token, _ = lexed[index] # Newlines after comments are emitted out of order. if next_token&.type == :COMMENT comment_newline_location = location next end value = nil when :tFLOAT value = parse_float(value) when :tIMAGINARY value = parse_complex(value) when :tINTEGER if value.start_with?("+") tokens << [:tUNARY_NUM, ["+", range(token.location.start_offset, token.location.start_offset + 1)]] location = range(token.location.start_offset + 1, token.location.end_offset) end value = parse_integer(value) when :tLABEL value.chomp!(":") when :tLABEL_END value.chomp!(":") when :tLCURLY type = :tLBRACE if state == EXPR_BEG | EXPR_LABEL when :tLPAREN2 type = :tLPAREN if tokens.empty? || LPAREN_CONVERSION_TOKEN_TYPES.include?(tokens.dig(-1, 0)) when :tNTH_REF value = parse_integer(value.delete_prefix("$")) when :tOP_ASGN value.chomp!("=") when :tRATIONAL value = parse_rational(value) when :tSPACE location = range(token.location.start_offset, token.location.start_offset + percent_array_leading_whitespace(value)) value = nil when :tSTRING_BEG next_token, _ = lexed[index] next_next_token, _ = lexed[index + 1] basic_quotes = value == '"' || value == "'" if basic_quotes && next_token&.type == :STRING_END next_location = token.location.join(next_token.location) type = :tSTRING value = "" location = range(next_location.start_offset, next_location.end_offset) index += 1 elsif value.start_with?("'", '"', "%") if next_token&.type == :STRING_CONTENT && next_next_token&.type == :STRING_END string_value = next_token.value if simplify_string?(string_value, value) next_location = token.location.join(next_next_token.location) if percent_array?(value) value = percent_array_unescape(string_value) else value = unescape_string(string_value, value) end type = :tSTRING location = range(next_location.start_offset, next_location.end_offset) index += 2 tokens << [type, [value, location]] next end end quote_stack.push(value) elsif token.type == :HEREDOC_START quote = value[2] == "-" || value[2] == "~" ? value[3] : value[2] heredoc_type = value[2] == "-" || value[2] == "~" ? value[2] : "" heredoc = HeredocData.new( identifier: value.match(/<<[-~]?["'`]?(?.*?)["'`]?\z/)[:heredoc_identifier], common_whitespace: 0, ) if quote == "`" type = :tXSTRING_BEG end # The parser gem trims whitespace from squiggly heredocs. We must record # the most common whitespace to later remove. if heredoc_type == "~" || heredoc_type == "`" heredoc.common_whitespace = calculate_heredoc_whitespace(index) end if quote == "'" || quote == '"' || quote == "`" value = "<<#{quote}" else value = '<<"' end heredoc_stack.push(heredoc) quote_stack.push(value) end when :tSTRING_CONTENT is_percent_array = percent_array?(quote_stack.last) if (lines = token.value.lines).one? # Prism usually emits a single token for strings with line continuations. # For squiggly heredocs they are not joined so we do that manually here. current_string = +"" current_length = 0 start_offset = token.location.start_offset while token.type == :STRING_CONTENT current_length += token.value.bytesize # Heredoc interpolation can have multiple STRING_CONTENT nodes on the same line. prev_token, _ = lexed[index - 2] if index - 2 >= 0 is_first_token_on_line = prev_token && token.location.start_line != prev_token.location.start_line # The parser gem only removes indentation when the heredoc is not nested not_nested = heredoc_stack.size == 1 if is_percent_array value = percent_array_unescape(token.value) elsif is_first_token_on_line && not_nested && (current_heredoc = heredoc_stack.last).common_whitespace > 0 value = trim_heredoc_whitespace(token.value, current_heredoc) end current_string << unescape_string(value, quote_stack.last) relevant_backslash_count = if quote_stack.last.start_with?("%W", "%I") 0 # the last backslash escapes the newline else token.value[/(\\{1,})\n/, 1]&.length || 0 end if relevant_backslash_count.even? || !interpolation?(quote_stack.last) tokens << [:tSTRING_CONTENT, [current_string, range(start_offset, start_offset + current_length)]] break end token, _ = lexed[index] index += 1 end else # When the parser gem encounters a line continuation inside of a multiline string, # it emits a single string node. The backslash (and remaining newline) is removed. current_line = +"" adjustment = 0 start_offset = token.location.start_offset emit = false lines.each.with_index do |line, index| chomped_line = line.chomp backslash_count = chomped_line[/\\{1,}\z/]&.length || 0 is_interpolation = interpolation?(quote_stack.last) if backslash_count.odd? && (is_interpolation || is_percent_array) if is_percent_array current_line << percent_array_unescape(line) adjustment += 1 else chomped_line.delete_suffix!("\\") current_line << chomped_line adjustment += 2 end # If the string ends with a line continuation emit the remainder emit = index == lines.count - 1 else current_line << line emit = true end if emit end_offset = start_offset + current_line.bytesize + adjustment tokens << [:tSTRING_CONTENT, [unescape_string(current_line, quote_stack.last), range(start_offset, end_offset)]] start_offset = end_offset current_line = +"" adjustment = 0 end end end next when :tSTRING_DVAR value = nil when :tSTRING_END if token.type == :HEREDOC_END && value.end_with?("\n") newline_length = value.end_with?("\r\n") ? 2 : 1 value = heredoc_stack.pop.identifier location = range(token.location.start_offset, token.location.end_offset - newline_length) elsif token.type == :REGEXP_END value = value[0] location = range(token.location.start_offset, token.location.start_offset + 1) end if percent_array?(quote_stack.pop) prev_token, _ = lexed[index - 2] if index - 2 >= 0 empty = %i[PERCENT_LOWER_I PERCENT_LOWER_W PERCENT_UPPER_I PERCENT_UPPER_W].include?(prev_token&.type) ends_with_whitespace = prev_token&.type == :WORDS_SEP # parser always emits a space token after content in a percent array, even if no actual whitespace is present. if !empty && !ends_with_whitespace tokens << [:tSPACE, [nil, range(token.location.start_offset, token.location.start_offset)]] end end when :tSYMBEG if (next_token = lexed[index]&.first) && next_token.type != :STRING_CONTENT && next_token.type != :EMBEXPR_BEGIN && next_token.type != :EMBVAR && next_token.type != :STRING_END next_location = token.location.join(next_token.location) type = :tSYMBOL value = next_token.value value = { "~@" => "~", "!@" => "!" }.fetch(value, value) location = range(next_location.start_offset, next_location.end_offset) index += 1 else quote_stack.push(value) end when :tFID if !tokens.empty? && tokens.dig(-1, 0) == :kDEF type = :tIDENTIFIER end when :tXSTRING_BEG if (next_token = lexed[index]&.first) && !%i[STRING_CONTENT STRING_END EMBEXPR_BEGIN].include?(next_token.type) # self.`() type = :tBACK_REF2 end quote_stack.push(value) when :tSYMBOLS_BEG, :tQSYMBOLS_BEG, :tWORDS_BEG, :tQWORDS_BEG if (next_token = lexed[index]&.first) && next_token.type == :WORDS_SEP index += 1 end quote_stack.push(value) when :tREGEXP_BEG quote_stack.push(value) end tokens << [type, [value, location]] if token.type == :REGEXP_END tokens << [:tREGEXP_OPT, [token.value[1..], range(token.location.start_offset + 1, token.location.end_offset)]] end end tokens end private # Creates a new parser range, taking prisms byte offsets into account def range(start_offset, end_offset) Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset]) end # Parse an integer from the string representation. def parse_integer(value) Integer(value) rescue ArgumentError 0 end # Parse a float from the string representation. def parse_float(value) Float(value) rescue ArgumentError 0.0 end # Parse a complex from the string representation. def parse_complex(value) value.chomp!("i") if value.end_with?("r") Complex(0, parse_rational(value)) elsif value.start_with?(/0[BbOoDdXx]/) Complex(0, parse_integer(value)) else Complex(0, value) end rescue ArgumentError 0i end # Parse a rational from the string representation. def parse_rational(value) value.chomp!("r") if value.start_with?(/0[BbOoDdXx]/) Rational(parse_integer(value)) else Rational(value) end rescue ArgumentError 0r end # Wonky heredoc tab/spaces rules. # https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L10548-L10558 def calculate_heredoc_whitespace(heredoc_token_index) next_token_index = heredoc_token_index nesting_level = 0 previous_line = -1 result = Float::MAX while (next_token = lexed[next_token_index]&.first) next_token_index += 1 next_next_token, _ = lexed[next_token_index] first_token_on_line = next_token.location.start_column == 0 # String content inside nested heredocs and interpolation is ignored if next_token.type == :HEREDOC_START || next_token.type == :EMBEXPR_BEGIN # When interpolation is the first token of a line there is no string # content to check against. There will be no common whitespace. if nesting_level == 0 && first_token_on_line result = 0 end nesting_level += 1 elsif next_token.type == :HEREDOC_END || next_token.type == :EMBEXPR_END nesting_level -= 1 # When we encountered the matching heredoc end, we can exit break if nesting_level == -1 elsif next_token.type == :STRING_CONTENT && nesting_level == 0 && first_token_on_line common_whitespace = 0 next_token.value[/^\s*/].each_char do |char| if char == "\t" common_whitespace = (common_whitespace / 8 + 1) * 8; else common_whitespace += 1 end end is_first_token_on_line = next_token.location.start_line != previous_line # Whitespace is significant if followed by interpolation whitespace_only = common_whitespace == next_token.value.length && next_next_token&.location&.start_line != next_token.location.start_line if is_first_token_on_line && !whitespace_only && common_whitespace < result result = common_whitespace previous_line = next_token.location.start_line end end end result end # Wonky heredoc tab/spaces rules. # https://github.com/ruby/prism/blob/v1.3.0/src/prism.c#L16528-L16545 def trim_heredoc_whitespace(string, heredoc) trimmed_whitespace = 0 trimmed_characters = 0 while (string[trimmed_characters] == "\t" || string[trimmed_characters] == " ") && trimmed_whitespace < heredoc.common_whitespace if string[trimmed_characters] == "\t" trimmed_whitespace = (trimmed_whitespace / 8 + 1) * 8; break if trimmed_whitespace > heredoc.common_whitespace else trimmed_whitespace += 1 end trimmed_characters += 1 end string[trimmed_characters..] end # Escape sequences that have special and should appear unescaped in the resulting string. ESCAPES = { "a" => "\a", "b" => "\b", "e" => "\e", "f" => "\f", "n" => "\n", "r" => "\r", "s" => "\s", "t" => "\t", "v" => "\v", "\\" => "\\" }.freeze private_constant :ESCAPES # When one of these delimiters is encountered, then the other # one is allowed to be escaped as well. DELIMITER_SYMETRY = { "[" => "]", "(" => ")", "{" => "}", "<" => ">" }.freeze private_constant :DELIMITER_SYMETRY # https://github.com/whitequark/parser/blob/v3.3.6.0/lib/parser/lexer-strings.rl#L14 REGEXP_META_CHARACTERS = ["\\", "$", "(", ")", "*", "+", ".", "<", ">", "?", "[", "]", "^", "{", "|", "}"] private_constant :REGEXP_META_CHARACTERS # Apply Ruby string escaping rules def unescape_string(string, quote) # In single-quoted heredocs, everything is taken literally. return string if quote == "<<'" # OPTIMIZATION: Assume that few strings need escaping to speed up the common case. return string unless string.include?("\\") # Enclosing character for the string. `"` for `"foo"`, `{` for `%w{foo}`, etc. delimiter = quote[-1] if regexp?(quote) # Should be escaped handled to single-quoted heredocs. The only character that is # allowed to be escaped is the delimiter, except when that also has special meaning # in the regexp. Since all the symetry delimiters have special meaning, they don't need # to be considered separately. if REGEXP_META_CHARACTERS.include?(delimiter) string else # There can never be an even amount of backslashes. It would be a syntax error. string.gsub(/\\(#{Regexp.escape(delimiter)})/, '\1') end elsif interpolation?(quote) # Appending individual escape sequences may force the string out of its intended # encoding. Start out with binary and force it back later. result = "".b scanner = StringScanner.new(string) while (skipped = scanner.skip_until(/\\/)) # Append what was just skipped over, excluding the found backslash. result.append_as_bytes(string.byteslice(scanner.pos - skipped, skipped - 1)) escape_read(result, scanner, false, false) end # Add remaining chars result.append_as_bytes(string.byteslice(scanner.pos..)) result.force_encoding(source_buffer.source.encoding) else delimiters = Regexp.escape("#{delimiter}#{DELIMITER_SYMETRY[delimiter]}") string.gsub(/\\([\\#{delimiters}])/, '\1') end end # Certain strings are merged into a single string token. def simplify_string?(value, quote) case quote when "'" # Only simplify 'foo' !value.include?("\n") when '"' # Simplify when every line ends with a line continuation, or it is the last line value.lines.all? do |line| !line.end_with?("\n") || line[/(\\*)$/, 1]&.length&.odd? end else # %q and similar are never simplified false end end # Escape a byte value, given the control and meta flags. def escape_build(value, control, meta) value &= 0x9f if control value |= 0x80 if meta value end # Read an escape out of the string scanner, given the control and meta # flags, and push the unescaped value into the result. def escape_read(result, scanner, control, meta) if scanner.skip("\n") # Line continuation elsif (value = ESCAPES[scanner.peek(1)]) # Simple single-character escape sequences like \n result.append_as_bytes(value) scanner.pos += 1 elsif (value = scanner.scan(/[0-7]{1,3}/)) # \nnn result.append_as_bytes(escape_build(value.to_i(8), control, meta)) elsif (value = scanner.scan(/x[0-9a-fA-F]{1,2}/)) # \xnn result.append_as_bytes(escape_build(value[1..].to_i(16), control, meta)) elsif (value = scanner.scan(/u[0-9a-fA-F]{4}/)) # \unnnn result.append_as_bytes(value[1..].hex.chr(Encoding::UTF_8)) elsif scanner.skip("u{}") # https://github.com/whitequark/parser/issues/856 elsif (value = scanner.scan(/u{.*?}/)) # \u{nnnn ...} value[2..-2].split.each do |unicode| result.append_as_bytes(unicode.hex.chr(Encoding::UTF_8)) end elsif (value = scanner.scan(/c\\?(?=[[:print:]])|C-\\?(?=[[:print:]])/)) # \cx or \C-x where x is an ASCII printable character escape_read(result, scanner, true, meta) elsif (value = scanner.scan(/M-\\?(?=[[:print:]])/)) # \M-x where x is an ASCII printable character escape_read(result, scanner, control, true) elsif (byte = scanner.scan_byte) # Something else after an escape. if control && byte == 0x3f # ASCII '?' result.append_as_bytes(escape_build(0x7f, false, meta)) else result.append_as_bytes(escape_build(byte, control, meta)) end end end # In a percent array, certain whitespace can be preceeded with a backslash, # causing the following characters to be part of the previous element. def percent_array_unescape(string) string.gsub(/(\\)+[ \f\n\r\t\v]/) do |full_match| full_match.delete_prefix!("\\") if Regexp.last_match[1].length.odd? full_match end end # For %-arrays whitespace, the parser gem only considers whitespace before the newline. def percent_array_leading_whitespace(string) return 1 if string.start_with?("\n") leading_whitespace = 0 string.each_char do |c| break if c == "\n" leading_whitespace += 1 end leading_whitespace end # Determine if characters preceeded by a backslash should be escaped or not def interpolation?(quote) !quote.end_with?("'") && !quote.start_with?("%q", "%w", "%i", "%s") end # Regexp allow interpolation but are handled differently during unescaping def regexp?(quote) quote == "/" || quote.start_with?("%r") end # Determine if the string is part of a %-style array. def percent_array?(quote) quote.start_with?("%w", "%W", "%i", "%I") end end end end end PK!׫zparser/builder.rbnu[# frozen_string_literal: true # :markup: markdown module Prism module Translation class Parser # A builder that knows how to convert more modern Ruby syntax # into whitequark/parser gem's syntax tree. class Builder < ::Parser::Builders::Default # It represents the `it` block argument, which is not yet implemented in the Parser gem. def itarg n(:itarg, [:it], nil) end # The following three lines have been added to support the `it` block parameter syntax in the source code below. # # if args.type == :itarg # block_type = :itblock # args = :it # # https://github.com/whitequark/parser/blob/v3.3.7.1/lib/parser/builders/default.rb#L1122-L1155 def block(method_call, begin_t, args, body, end_t) _receiver, _selector, *call_args = *method_call if method_call.type == :yield diagnostic :error, :block_given_to_yield, nil, method_call.loc.keyword, [loc(begin_t)] end last_arg = call_args.last if last_arg && (last_arg.type == :block_pass || last_arg.type == :forwarded_args) diagnostic :error, :block_and_blockarg, nil, last_arg.loc.expression, [loc(begin_t)] end if args.type == :itarg block_type = :itblock args = :it elsif args.type == :numargs block_type = :numblock args = args.children[0] else block_type = :block end if [:send, :csend, :index, :super, :zsuper, :lambda].include?(method_call.type) n(block_type, [ method_call, args, body ], block_map(method_call.loc.expression, begin_t, end_t)) else # Code like "return foo 1 do end" is reduced in a weird sequence. # Here, method_call is actually (return). actual_send, = *method_call block = n(block_type, [ actual_send, args, body ], block_map(actual_send.loc.expression, begin_t, end_t)) n(method_call.type, [ block ], method_call.loc.with_expression(join_exprs(method_call, block))) end end end end end end PK! O0s's'parser/compiler.rbnu[# frozen_string_literal: true # :markup: markdown module Prism module Translation class Parser # A visitor that knows how to convert a prism syntax tree into the # whitequark/parser gem's syntax tree. class Compiler < ::Prism::Compiler # Raised when the tree is malformed or there is a bug in the compiler. class CompilationError < StandardError end # The Parser::Base instance that is being used to build the AST. attr_reader :parser # The Parser::Builders::Default instance that is being used to build the # AST. attr_reader :builder # The Parser::Source::Buffer instance that is holding a reference to the # source code. attr_reader :source_buffer # The offset cache that is used to map between byte and character # offsets in the file. attr_reader :offset_cache # The types of values that can be forwarded in the current scope. attr_reader :forwarding # Whether or not the current node is in a destructure. attr_reader :in_destructure # Whether or not the current node is in a pattern. attr_reader :in_pattern # Initialize a new compiler with the given parser, offset cache, and # options. def initialize(parser, offset_cache, forwarding: [], in_destructure: false, in_pattern: false) @parser = parser @builder = parser.builder @source_buffer = parser.source_buffer @offset_cache = offset_cache @forwarding = forwarding @in_destructure = in_destructure @in_pattern = in_pattern end # alias foo bar # ^^^^^^^^^^^^^ def visit_alias_method_node(node) builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name)) end # alias $foo $bar # ^^^^^^^^^^^^^^^ def visit_alias_global_variable_node(node) builder.alias(token(node.keyword_loc), visit(node.new_name), visit(node.old_name)) end # foo => bar | baz # ^^^^^^^^^ def visit_alternation_pattern_node(node) builder.match_alt(visit(node.left), token(node.operator_loc), visit(node.right)) end # a and b # ^^^^^^^ def visit_and_node(node) builder.logical_op(:and, visit(node.left), token(node.operator_loc), visit(node.right)) end # [] # ^^ def visit_array_node(node) if node.opening&.start_with?("%w", "%W", "%i", "%I") elements = node.elements.flat_map do |element| if element.is_a?(StringNode) if element.content.include?("\n") string_nodes_from_line_continuations(element.unescaped, element.content, element.content_loc.start_offset, node.opening) else [builder.string_internal([element.unescaped, srange(element.content_loc)])] end elsif element.is_a?(InterpolatedStringNode) builder.string_compose( token(element.opening_loc), string_nodes_from_interpolation(element, node.opening), token(element.closing_loc) ) else [visit(element)] end end else elements = visit_all(node.elements) end builder.array(token(node.opening_loc), elements, token(node.closing_loc)) end # foo => [bar] # ^^^^^ def visit_array_pattern_node(node) elements = [*node.requireds] elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) elements.concat(node.posts) visited = visit_all(elements) if node.rest.is_a?(ImplicitRestNode) visited[-1] = builder.match_with_trailing_comma(visited[-1], token(node.rest.location)) end if node.constant if visited.empty? builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)), token(node.closing_loc)) else builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.array_pattern(nil, visited, nil), token(node.closing_loc)) end else builder.array_pattern(token(node.opening_loc), visited, token(node.closing_loc)) end end # foo(bar) # ^^^ def visit_arguments_node(node) visit_all(node.arguments) end # { a: 1 } # ^^^^ def visit_assoc_node(node) key = node.key if node.value.is_a?(ImplicitNode) if in_pattern if key.is_a?(SymbolNode) if key.opening.nil? builder.match_hash_var([key.unescaped, srange(key.location)]) else builder.match_hash_var_from_str(token(key.opening_loc), [builder.string_internal([key.unescaped, srange(key.value_loc)])], token(key.closing_loc)) end else builder.match_hash_var_from_str(token(key.opening_loc), visit_all(key.parts), token(key.closing_loc)) end else value = node.value.value implicit_value = if value.is_a?(CallNode) builder.call_method(nil, nil, [value.name, srange(value.message_loc)]) elsif value.is_a?(ConstantReadNode) builder.const([value.name, srange(key.value_loc)]) else builder.ident([value.name, srange(key.value_loc)]).updated(:lvar) end builder.pair_keyword([key.unescaped, srange(key)], implicit_value) end elsif node.operator_loc builder.pair(visit(key), token(node.operator_loc), visit(node.value)) elsif key.is_a?(SymbolNode) && key.opening_loc.nil? builder.pair_keyword([key.unescaped, srange(key.location)], visit(node.value)) else parts = if key.is_a?(SymbolNode) [builder.string_internal([key.unescaped, srange(key.value_loc)])] else visit_all(key.parts) end builder.pair_quoted(token(key.opening_loc), parts, token(key.closing_loc), visit(node.value)) end end # def foo(**); bar(**); end # ^^ # # { **foo } # ^^^^^ def visit_assoc_splat_node(node) if in_pattern builder.match_rest(token(node.operator_loc), token(node.value&.location)) elsif node.value.nil? && forwarding.include?(:**) builder.forwarded_kwrestarg(token(node.operator_loc)) else builder.kwsplat(token(node.operator_loc), visit(node.value)) end end # $+ # ^^ def visit_back_reference_read_node(node) builder.back_ref(token(node.location)) end # begin end # ^^^^^^^^^ def visit_begin_node(node) rescue_bodies = [] if (rescue_clause = node.rescue_clause) begin find_start_offset = (rescue_clause.reference&.location || rescue_clause.exceptions.last&.location || rescue_clause.keyword_loc).end_offset find_end_offset = ( rescue_clause.statements&.location&.start_offset || rescue_clause.subsequent&.location&.start_offset || node.else_clause&.location&.start_offset || node.ensure_clause&.location&.start_offset || node.end_keyword_loc&.start_offset || find_start_offset + 1 ) rescue_bodies << builder.rescue_body( token(rescue_clause.keyword_loc), rescue_clause.exceptions.any? ? builder.array(nil, visit_all(rescue_clause.exceptions), nil) : nil, token(rescue_clause.operator_loc), visit(rescue_clause.reference), srange_semicolon(find_start_offset, find_end_offset), visit(rescue_clause.statements) ) end until (rescue_clause = rescue_clause.subsequent).nil? end begin_body = builder.begin_body( visit(node.statements), rescue_bodies, token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.ensure_clause&.ensure_keyword_loc), visit(node.ensure_clause&.statements) ) if node.begin_keyword_loc builder.begin_keyword(token(node.begin_keyword_loc), begin_body, token(node.end_keyword_loc)) else begin_body end end # foo(&bar) # ^^^^ def visit_block_argument_node(node) builder.block_pass(token(node.operator_loc), visit(node.expression)) end # foo { |; bar| } # ^^^ def visit_block_local_variable_node(node) builder.shadowarg(token(node.location)) end # A block on a keyword or method call. def visit_block_node(node) raise CompilationError, "Cannot directly compile block nodes" end # def foo(&bar); end # ^^^^ def visit_block_parameter_node(node) builder.blockarg(token(node.operator_loc), token(node.name_loc)) end # A block's parameters. def visit_block_parameters_node(node) [*visit(node.parameters)].concat(visit_all(node.locals)) end # break # ^^^^^ # # break foo # ^^^^^^^^^ def visit_break_node(node) builder.keyword_cmd(:break, token(node.keyword_loc), nil, visit(node.arguments) || [], nil) end # foo # ^^^ # # foo.bar # ^^^^^^^ # # foo.bar() {} # ^^^^^^^^^^^^ def visit_call_node(node) name = node.name arguments = node.arguments&.arguments || [] block = node.block if block.is_a?(BlockArgumentNode) arguments = [*arguments, block] block = nil end if node.call_operator_loc.nil? case name when :-@ case (receiver = node.receiver).type when :integer_node, :float_node, :rational_node, :imaginary_node return visit(numeric_negate(node.message_loc, receiver)) end when :! return visit_block(builder.not_op(token(node.message_loc), token(node.opening_loc), visit(node.receiver), token(node.closing_loc)), block) when :=~ if (receiver = node.receiver).is_a?(RegularExpressionNode) return builder.match_op(visit(receiver), token(node.message_loc), visit(node.arguments.arguments.first)) end when :[] return visit_block(builder.index(visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc)), block) when :[]= if node.message != "[]=" && node.arguments && block.nil? && !node.safe_navigation? arguments = node.arguments.arguments[...-1] arguments << node.block if node.block return visit_block( builder.assign( builder.index_asgn( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc), ), token(node.equal_loc), visit(node.arguments.arguments.last) ), block ) end end end message_loc = node.message_loc call_operator_loc = node.call_operator_loc call_operator = [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)] if call_operator_loc visit_block( if name.end_with?("=") && !message_loc.slice.end_with?("=") && node.arguments && block.nil? builder.assign( builder.attr_asgn(visit(node.receiver), call_operator, token(message_loc)), token(node.equal_loc), visit(node.arguments.arguments.last) ) else builder.call_method( visit(node.receiver), call_operator, message_loc ? [node.name, srange(message_loc)] : nil, token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ) end, block ) end # foo.bar += baz # ^^^^^^^^^^^^^^^ def visit_call_operator_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo.bar &&= baz # ^^^^^^^^^^^^^^^ def visit_call_and_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo.bar ||= baz # ^^^^^^^^^^^^^^^ def visit_call_or_write_node(node) call_operator_loc = node.call_operator_loc builder.op_assign( builder.call_method( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], node.message_loc ? [node.read_name, srange(node.message_loc)] : nil, nil, [], nil ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo.bar, = 1 # ^^^^^^^ def visit_call_target_node(node) call_operator_loc = node.call_operator_loc builder.attr_asgn( visit(node.receiver), call_operator_loc.nil? ? nil : [{ "." => :dot, "&." => :anddot, "::" => "::" }.fetch(call_operator_loc.slice), srange(call_operator_loc)], token(node.message_loc) ) end # foo => bar => baz # ^^^^^^^^^^ def visit_capture_pattern_node(node) builder.match_as(visit(node.value), token(node.operator_loc), visit(node.target)) end # case foo; when bar; end # ^^^^^^^^^^^^^^^^^^^^^^^ def visit_case_node(node) builder.case( token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.end_keyword_loc) ) end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_case_match_node(node) builder.case_match( token(node.case_keyword_loc), visit(node.predicate), visit_all(node.conditions), token(node.else_clause&.else_keyword_loc), visit(node.else_clause), token(node.end_keyword_loc) ) end # class Foo; end # ^^^^^^^^^^^^^^ def visit_class_node(node) builder.def_class( token(node.class_keyword_loc), visit(node.constant_path), token(node.inheritance_operator_loc), visit(node.superclass), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # @@foo # ^^^^^ def visit_class_variable_read_node(node) builder.cvar(token(node.location)) end # @@foo = 1 # ^^^^^^^^^ def visit_class_variable_write_node(node) builder.assign( builder.assignable(builder.cvar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # @@foo += bar # ^^^^^^^^^^^^ def visit_class_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @@foo &&= bar # ^^^^^^^^^^^^^ def visit_class_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @@foo ||= bar # ^^^^^^^^^^^^^ def visit_class_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.cvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @@foo, = bar # ^^^^^ def visit_class_variable_target_node(node) builder.assignable(builder.cvar(token(node.location))) end # Foo # ^^^ def visit_constant_read_node(node) builder.const([node.name, srange(node.location)]) end # Foo = 1 # ^^^^^^^ # # Foo, Bar = 1 # ^^^ ^^^ def visit_constant_write_node(node) builder.assign(builder.assignable(builder.const([node.name, srange(node.name_loc)])), token(node.operator_loc), visit(node.value)) end # Foo += bar # ^^^^^^^^^^^ def visit_constant_operator_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo &&= bar # ^^^^^^^^^^^^ def visit_constant_and_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo ||= bar # ^^^^^^^^^^^^ def visit_constant_or_write_node(node) builder.op_assign( builder.assignable(builder.const([node.name, srange(node.name_loc)])), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo, = bar # ^^^ def visit_constant_target_node(node) builder.assignable(builder.const([node.name, srange(node.location)])) end # Foo::Bar # ^^^^^^^^ def visit_constant_path_node(node) if node.parent.nil? builder.const_global( token(node.delimiter_loc), [node.name, srange(node.name_loc)] ) else builder.const_fetch( visit(node.parent), token(node.delimiter_loc), [node.name, srange(node.name_loc)] ) end end # Foo::Bar = 1 # ^^^^^^^^^^^^ # # Foo::Foo, Bar::Bar = 1 # ^^^^^^^^ ^^^^^^^^ def visit_constant_path_write_node(node) builder.assign( builder.assignable(visit(node.target)), token(node.operator_loc), visit(node.value) ) end # Foo::Bar += baz # ^^^^^^^^^^^^^^^ def visit_constant_path_operator_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # Foo::Bar &&= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_and_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo::Bar ||= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_or_write_node(node) builder.op_assign( builder.assignable(visit(node.target)), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # Foo::Bar, = baz # ^^^^^^^^ def visit_constant_path_target_node(node) builder.assignable(visit_constant_path_node(node)) end # def foo; end # ^^^^^^^^^^^^ # # def self.foo; end # ^^^^^^^^^^^^^^^^^ def visit_def_node(node) if node.equal_loc if node.receiver builder.def_endless_singleton( token(node.def_keyword_loc), visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver), token(node.operator_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), token(node.equal_loc), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))) ) else builder.def_endless_method( token(node.def_keyword_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), token(node.equal_loc), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))) ) end elsif node.receiver builder.def_singleton( token(node.def_keyword_loc), visit(node.receiver.is_a?(ParenthesesNode) ? node.receiver.body : node.receiver), token(node.operator_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))), token(node.end_keyword_loc) ) else builder.def_method( token(node.def_keyword_loc), token(node.name_loc), builder.args(token(node.lparen_loc), visit(node.parameters) || [], token(node.rparen_loc), false), node.body&.accept(copy_compiler(forwarding: find_forwarding(node.parameters))), token(node.end_keyword_loc) ) end end # defined? a # ^^^^^^^^^^ # # defined?(a) # ^^^^^^^^^^^ def visit_defined_node(node) # Very weird circumstances here where something like: # # defined? # (1) # # gets parsed in Ruby as having only the `1` expression but in parser # it gets parsed as having a begin. In this case we need to synthesize # that begin to match parser's behavior. if node.lparen_loc && node.keyword_loc.join(node.lparen_loc).slice.include?("\n") builder.keyword_cmd( :defined?, token(node.keyword_loc), nil, [ builder.begin( token(node.lparen_loc), visit(node.value), token(node.rparen_loc) ) ], nil ) else builder.keyword_cmd( :defined?, token(node.keyword_loc), token(node.lparen_loc), [visit(node.value)], token(node.rparen_loc) ) end end # if foo then bar else baz end # ^^^^^^^^^^^^ def visit_else_node(node) visit(node.statements) end # "foo #{bar}" # ^^^^^^ def visit_embedded_statements_node(node) builder.begin( token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # "foo #@bar" # ^^^^^ def visit_embedded_variable_node(node) visit(node.variable) end # begin; foo; ensure; bar; end # ^^^^^^^^^^^^ def visit_ensure_node(node) raise CompilationError, "Cannot directly compile ensure nodes" end # false # ^^^^^ def visit_false_node(node) builder.false(token(node.location)) end # foo => [*, bar, *] # ^^^^^^^^^^^ def visit_find_pattern_node(node) elements = [node.left, *node.requireds, node.right] if node.constant builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.find_pattern(nil, visit_all(elements), nil), token(node.closing_loc)) else builder.find_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc)) end end # 1.0 # ^^^ def visit_float_node(node) visit_numeric(node, builder.float([node.value, srange(node.location)])) end # for foo in bar do end # ^^^^^^^^^^^^^^^^^^^^^ def visit_for_node(node) builder.for( token(node.for_keyword_loc), visit(node.index), token(node.in_keyword_loc), visit(node.collection), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_semicolon(node.collection.location.end_offset, (node.statements&.location || node.end_keyword_loc).start_offset) end, visit(node.statements), token(node.end_keyword_loc) ) end # def foo(...); bar(...); end # ^^^ def visit_forwarding_arguments_node(node) builder.forwarded_args(token(node.location)) end # def foo(...); end # ^^^ def visit_forwarding_parameter_node(node) builder.forward_arg(token(node.location)) end # super # ^^^^^ # # super {} # ^^^^^^^^ def visit_forwarding_super_node(node) visit_block( builder.keyword_cmd( :zsuper, ["super", srange_offsets(node.location.start_offset, node.location.start_offset + 5)] ), node.block ) end # $foo # ^^^^ def visit_global_variable_read_node(node) builder.gvar(token(node.location)) end # $foo = 1 # ^^^^^^^^ def visit_global_variable_write_node(node) builder.assign( builder.assignable(builder.gvar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # $foo += bar # ^^^^^^^^^^^ def visit_global_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # $foo &&= bar # ^^^^^^^^^^^^ def visit_global_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # $foo ||= bar # ^^^^^^^^^^^^ def visit_global_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.gvar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # $foo, = bar # ^^^^ def visit_global_variable_target_node(node) builder.assignable(builder.gvar([node.slice, srange(node.location)])) end # {} # ^^ def visit_hash_node(node) builder.associate( token(node.opening_loc), visit_all(node.elements), token(node.closing_loc) ) end # foo => {} # ^^ def visit_hash_pattern_node(node) elements = [*node.elements, *node.rest] if node.constant builder.const_pattern(visit(node.constant), token(node.opening_loc), builder.hash_pattern(nil, visit_all(elements), nil), token(node.closing_loc)) else builder.hash_pattern(token(node.opening_loc), visit_all(elements), token(node.closing_loc)) end end # if foo then bar end # ^^^^^^^^^^^^^^^^^^^ # # bar if foo # ^^^^^^^^^^ # # foo ? bar : baz # ^^^^^^^^^^^^^^^ def visit_if_node(node) if !node.if_keyword_loc builder.ternary( visit(node.predicate), token(node.then_keyword_loc), visit(node.statements), token(node.subsequent.else_keyword_loc), visit(node.subsequent) ) elsif node.if_keyword_loc.start_offset == node.location.start_offset builder.condition( token(node.if_keyword_loc), visit(node.predicate), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.subsequent&.location || node.end_keyword_loc).start_offset) end, visit(node.statements), case node.subsequent when IfNode token(node.subsequent.if_keyword_loc) when ElseNode token(node.subsequent.else_keyword_loc) end, visit(node.subsequent), if node.if_keyword != "elsif" token(node.end_keyword_loc) end ) else builder.condition_mod( visit(node.statements), visit(node.subsequent), token(node.if_keyword_loc), visit(node.predicate) ) end end # 1i # ^^ def visit_imaginary_node(node) visit_numeric(node, builder.complex([Complex(0, node.numeric.value), srange(node.location)])) end # { foo: } # ^^^^ def visit_implicit_node(node) raise CompilationError, "Cannot directly compile implicit nodes" end # foo { |bar,| } # ^ def visit_implicit_rest_node(node) raise CompilationError, "Cannot compile implicit rest nodes" end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_in_node(node) pattern = nil guard = nil case node.pattern when IfNode pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) } guard = builder.if_guard(token(node.pattern.if_keyword_loc), visit(node.pattern.predicate)) when UnlessNode pattern = within_pattern { |compiler| node.pattern.statements.accept(compiler) } guard = builder.unless_guard(token(node.pattern.keyword_loc), visit(node.pattern.predicate)) else pattern = within_pattern { |compiler| node.pattern.accept(compiler) } end builder.in_pattern( token(node.in_loc), pattern, guard, if (then_loc = node.then_loc) token(then_loc) else srange_semicolon(node.pattern.location.end_offset, node.statements&.location&.start_offset) end, visit(node.statements) ) end # foo[bar] += baz # ^^^^^^^^^^^^^^^ def visit_index_operator_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo[bar] &&= baz # ^^^^^^^^^^^^^^^^ def visit_index_and_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo[bar] ||= baz # ^^^^^^^^^^^^^^^^ def visit_index_or_write_node(node) arguments = node.arguments&.arguments || [] arguments << node.block if node.block builder.op_assign( builder.index( visit(node.receiver), token(node.opening_loc), visit_all(arguments), token(node.closing_loc) ), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo[bar], = 1 # ^^^^^^^^ def visit_index_target_node(node) builder.index_asgn( visit(node.receiver), token(node.opening_loc), visit_all(node.arguments&.arguments || []), token(node.closing_loc), ) end # @foo # ^^^^ def visit_instance_variable_read_node(node) builder.ivar(token(node.location)) end # @foo = 1 # ^^^^^^^^ def visit_instance_variable_write_node(node) builder.assign( builder.assignable(builder.ivar(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # @foo += bar # ^^^^^^^^^^^ def visit_instance_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # @foo &&= bar # ^^^^^^^^^^^^ def visit_instance_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @foo ||= bar # ^^^^^^^^^^^^ def visit_instance_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.ivar(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # @foo, = bar # ^^^^ def visit_instance_variable_target_node(node) builder.assignable(builder.ivar(token(node.location))) end # 1 # ^ def visit_integer_node(node) visit_numeric(node, builder.integer([node.value, srange(node.location)])) end # /foo #{bar}/ # ^^^^^^^^^^^^ def visit_interpolated_regular_expression_node(node) builder.regexp_compose( token(node.opening_loc), string_nodes_from_interpolation(node, node.opening), [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)], builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)]) ) end # if /foo #{bar}/ then end # ^^^^^^^^^^^^ alias visit_interpolated_match_last_line_node visit_interpolated_regular_expression_node # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) if node.heredoc? return visit_heredoc(node) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } end builder.string_compose( token(node.opening_loc), string_nodes_from_interpolation(node, node.opening), token(node.closing_loc) ) end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) builder.symbol_compose( token(node.opening_loc), string_nodes_from_interpolation(node, node.opening), token(node.closing_loc) ) end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) if node.heredoc? return visit_heredoc(node) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } end builder.xstring_compose( token(node.opening_loc), string_nodes_from_interpolation(node, node.opening), token(node.closing_loc) ) end # -> { it } # ^^ def visit_it_local_variable_read_node(node) builder.ident([:it, srange(node.location)]).updated(:lvar) end # -> { it } # ^^^^^^^^^ def visit_it_parameters_node(node) # FIXME: The builder _should_ always be a subclass of the prism builder. # Currently RuboCop passes in its own builder that always inherits from the # parser builder (which is lacking the `itarg` method). Once rubocop-ast # opts in to use the custom prism builder a warning can be emitted when # it is not the expected class, and eventually raise. # https://github.com/rubocop/rubocop-ast/pull/354 if builder.is_a?(Translation::Parser::Builder) builder.itarg else builder.args(nil, [], nil, false) end end # foo(bar: baz) # ^^^^^^^^ def visit_keyword_hash_node(node) builder.associate(nil, visit_all(node.elements), nil) end # def foo(**bar); end # ^^^^^ # # def foo(**); end # ^^ def visit_keyword_rest_parameter_node(node) builder.kwrestarg( token(node.operator_loc), node.name ? [node.name, srange(node.name_loc)] : nil ) end # -> {} # ^^^^^ def visit_lambda_node(node) parameters = node.parameters implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( builder.call_lambda(token(node.operator_loc)), [node.opening, srange(node.opening_loc)], if parameters.nil? builder.args(nil, [], nil, false) elsif implicit_parameters visit(node.parameters) else builder.args( token(node.parameters.opening_loc), visit(node.parameters), token(node.parameters.closing_loc), false ) end, visit(node.body), [node.closing, srange(node.closing_loc)] ) end # foo # ^^^ def visit_local_variable_read_node(node) builder.ident([node.name, srange(node.location)]).updated(:lvar) end # foo = 1 # ^^^^^^^ def visit_local_variable_write_node(node) builder.assign( builder.assignable(builder.ident(token(node.name_loc))), token(node.operator_loc), visit(node.value) ) end # foo += bar # ^^^^^^^^^^ def visit_local_variable_operator_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.binary_operator_loc.slice.chomp("="), srange(node.binary_operator_loc)], visit(node.value) ) end # foo &&= bar # ^^^^^^^^^^^ def visit_local_variable_and_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo ||= bar # ^^^^^^^^^^^ def visit_local_variable_or_write_node(node) builder.op_assign( builder.assignable(builder.ident(token(node.name_loc))), [node.operator_loc.slice.chomp("="), srange(node.operator_loc)], visit(node.value) ) end # foo, = bar # ^^^ def visit_local_variable_target_node(node) if in_pattern builder.assignable(builder.match_var([node.name, srange(node.location)])) else builder.assignable(builder.ident(token(node.location))) end end # foo in bar # ^^^^^^^^^^ def visit_match_predicate_node(node) builder.match_pattern_p( visit(node.value), token(node.operator_loc), within_pattern { |compiler| node.pattern.accept(compiler) } ) end # foo => bar # ^^^^^^^^^^ def visit_match_required_node(node) builder.match_pattern( visit(node.value), token(node.operator_loc), within_pattern { |compiler| node.pattern.accept(compiler) } ) end # /(?foo)/ =~ bar # ^^^^^^^^^^^^^^^^^^^^ def visit_match_write_node(node) builder.match_op( visit(node.call.receiver), token(node.call.message_loc), visit(node.call.arguments.arguments.first) ) end # A node that is missing from the syntax tree. This is only used in the # case of a syntax error. The parser gem doesn't have such a concept, so # we invent our own here. def visit_missing_node(node) ::AST::Node.new(:missing, [], location: ::Parser::Source::Map.new(srange(node.location))) end # module Foo; end # ^^^^^^^^^^^^^^^ def visit_module_node(node) builder.def_module( token(node.module_keyword_loc), visit(node.constant_path), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # foo, bar = baz # ^^^^^^^^ def visit_multi_target_node(node) builder.multi_lhs( token(node.lparen_loc), visit_all(multi_target_elements(node)), token(node.rparen_loc) ) end # foo, bar = baz # ^^^^^^^^^^^^^^ def visit_multi_write_node(node) elements = multi_target_elements(node) if elements.length == 1 && elements.first.is_a?(MultiTargetNode) && !node.rest elements = multi_target_elements(elements.first) end builder.multi_assign( builder.multi_lhs( token(node.lparen_loc), visit_all(elements), token(node.rparen_loc) ), token(node.operator_loc), visit(node.value) ) end # next # ^^^^ # # next foo # ^^^^^^^^ def visit_next_node(node) builder.keyword_cmd( :next, token(node.keyword_loc), nil, visit(node.arguments) || [], nil ) end # nil # ^^^ def visit_nil_node(node) builder.nil(token(node.location)) end # def foo(**nil); end # ^^^^^ def visit_no_keywords_parameter_node(node) if in_pattern builder.match_nil_pattern(token(node.operator_loc), token(node.keyword_loc)) else builder.kwnilarg(token(node.operator_loc), token(node.keyword_loc)) end end # -> { _1 + _2 } # ^^^^^^^^^^^^^^ def visit_numbered_parameters_node(node) builder.numargs(node.maximum) end # $1 # ^^ def visit_numbered_reference_read_node(node) builder.nth_ref([node.number, srange(node.location)]) end # def foo(bar: baz); end # ^^^^^^^^ def visit_optional_keyword_parameter_node(node) builder.kwoptarg([node.name, srange(node.name_loc)], visit(node.value)) end # def foo(bar = 1); end # ^^^^^^^ def visit_optional_parameter_node(node) builder.optarg(token(node.name_loc), token(node.operator_loc), visit(node.value)) end # a or b # ^^^^^^ def visit_or_node(node) builder.logical_op(:or, visit(node.left), token(node.operator_loc), visit(node.right)) end # def foo(bar, *baz); end # ^^^^^^^^^ def visit_parameters_node(node) params = [] if node.requireds.any? node.requireds.each do |required| params << if required.is_a?(RequiredParameterNode) visit(required) else required.accept(copy_compiler(in_destructure: true)) end end end params.concat(visit_all(node.optionals)) if node.optionals.any? params << visit(node.rest) if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) if node.posts.any? node.posts.each do |post| params << if post.is_a?(RequiredParameterNode) visit(post) else post.accept(copy_compiler(in_destructure: true)) end end end params.concat(visit_all(node.keywords)) if node.keywords.any? params << visit(node.keyword_rest) if !node.keyword_rest.nil? params << visit(node.block) if !node.block.nil? params end # () # ^^ # # (1) # ^^^ def visit_parentheses_node(node) builder.begin( token(node.opening_loc), visit(node.body), token(node.closing_loc) ) end # foo => ^(bar) # ^^^^^^ def visit_pinned_expression_node(node) parts = node.expression.accept(copy_compiler(in_pattern: false)) # Don't treat * and similar as match_rest expression = builder.begin(token(node.lparen_loc), parts, token(node.rparen_loc)) builder.pin(token(node.operator_loc), expression) end # foo = 1 and bar => ^foo # ^^^^ def visit_pinned_variable_node(node) builder.pin(token(node.operator_loc), visit(node.variable)) end # END {} def visit_post_execution_node(node) builder.postexe( token(node.keyword_loc), token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # BEGIN {} def visit_pre_execution_node(node) builder.preexe( token(node.keyword_loc), token(node.opening_loc), visit(node.statements), token(node.closing_loc) ) end # The top-level program node. def visit_program_node(node) visit(node.statements) end # 0..5 # ^^^^ def visit_range_node(node) if node.exclude_end? builder.range_exclusive( visit(node.left), token(node.operator_loc), visit(node.right) ) else builder.range_inclusive( visit(node.left), token(node.operator_loc), visit(node.right) ) end end # if foo .. bar; end # ^^^^^^^^^^ alias visit_flip_flop_node visit_range_node # 1r # ^^ def visit_rational_node(node) visit_numeric(node, builder.rational([node.value, srange(node.location)])) end # redo # ^^^^ def visit_redo_node(node) builder.keyword_cmd(:redo, token(node.location)) end # /foo/ # ^^^^^ def visit_regular_expression_node(node) parts = if node.content == "" [] elsif node.content.include?("\n") string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening) else [builder.string_internal([node.unescaped, srange(node.content_loc)])] end builder.regexp_compose( token(node.opening_loc), parts, [node.closing[0], srange_offsets(node.closing_loc.start_offset, node.closing_loc.start_offset + 1)], builder.regexp_options([node.closing[1..], srange_offsets(node.closing_loc.start_offset + 1, node.closing_loc.end_offset)]) ) end # if /foo/ then end # ^^^^^ alias visit_match_last_line_node visit_regular_expression_node # def foo(bar:); end # ^^^^ def visit_required_keyword_parameter_node(node) builder.kwarg([node.name, srange(node.name_loc)]) end # def foo(bar); end # ^^^ def visit_required_parameter_node(node) builder.arg(token(node.location)) end # foo rescue bar # ^^^^^^^^^^^^^^ def visit_rescue_modifier_node(node) builder.begin_body( visit(node.expression), [ builder.rescue_body( token(node.keyword_loc), nil, nil, nil, nil, visit(node.rescue_expression) ) ] ) end # begin; rescue; end # ^^^^^^^ def visit_rescue_node(node) raise CompilationError, "Cannot directly compile rescue nodes" end # def foo(*bar); end # ^^^^ # # def foo(*); end # ^ def visit_rest_parameter_node(node) builder.restarg(token(node.operator_loc), token(node.name_loc)) end # retry # ^^^^^ def visit_retry_node(node) builder.keyword_cmd(:retry, token(node.location)) end # return # ^^^^^^ # # return 1 # ^^^^^^^^ def visit_return_node(node) builder.keyword_cmd( :return, token(node.keyword_loc), nil, visit(node.arguments) || [], nil ) end # self # ^^^^ def visit_self_node(node) builder.self(token(node.location)) end # A shareable constant. def visit_shareable_constant_node(node) visit(node.write) end # class << self; end # ^^^^^^^^^^^^^^^^^^ def visit_singleton_class_node(node) builder.def_sclass( token(node.class_keyword_loc), token(node.operator_loc), visit(node.expression), node.body&.accept(copy_compiler(forwarding: [])), token(node.end_keyword_loc) ) end # __ENCODING__ # ^^^^^^^^^^^^ def visit_source_encoding_node(node) builder.accessible(builder.__ENCODING__(token(node.location))) end # __FILE__ # ^^^^^^^^ def visit_source_file_node(node) builder.accessible(builder.__FILE__(token(node.location))) end # __LINE__ # ^^^^^^^^ def visit_source_line_node(node) builder.accessible(builder.__LINE__(token(node.location))) end # foo(*bar) # ^^^^ # # def foo((bar, *baz)); end # ^^^^ # # def foo(*); bar(*); end # ^ def visit_splat_node(node) if node.expression.nil? && forwarding.include?(:*) builder.forwarded_restarg(token(node.operator_loc)) elsif in_destructure builder.restarg(token(node.operator_loc), token(node.expression&.location)) elsif in_pattern builder.match_rest(token(node.operator_loc), token(node.expression&.location)) else builder.splat(token(node.operator_loc), visit(node.expression)) end end # A list of statements. def visit_statements_node(node) builder.compstmt(visit_all(node.body)) end # "foo" # ^^^^^ def visit_string_node(node) if node.heredoc? visit_heredoc(node.to_interpolated) { |children, closing| builder.string_compose(token(node.opening_loc), children, closing) } elsif node.opening == "?" builder.character([node.unescaped, srange(node.location)]) elsif node.opening&.start_with?("%") && node.unescaped.empty? builder.string_compose(token(node.opening_loc), [], token(node.closing_loc)) else parts = if node.content.include?("\n") string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening) else [builder.string_internal([node.unescaped, srange(node.content_loc)])] end builder.string_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end end # super(foo) # ^^^^^^^^^^ def visit_super_node(node) arguments = node.arguments&.arguments || [] block = node.block if block.is_a?(BlockArgumentNode) arguments = [*arguments, block] block = nil end visit_block( builder.keyword_cmd( :super, token(node.keyword_loc), token(node.lparen_loc), visit_all(arguments), token(node.rparen_loc) ), block ) end # :foo # ^^^^ def visit_symbol_node(node) if node.closing_loc.nil? if node.opening_loc.nil? builder.symbol_internal([node.unescaped, srange(node.location)]) else builder.symbol([node.unescaped, srange(node.location)]) end else parts = if node.value == "" [] elsif node.value.include?("\n") string_nodes_from_line_continuations(node.unescaped, node.value, node.value_loc.start_offset, node.opening) else [builder.string_internal([node.unescaped, srange(node.value_loc)])] end builder.symbol_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end end # true # ^^^^ def visit_true_node(node) builder.true(token(node.location)) end # undef foo # ^^^^^^^^^ def visit_undef_node(node) builder.undef_method(token(node.keyword_loc), visit_all(node.names)) end # unless foo; bar end # ^^^^^^^^^^^^^^^^^^^ # # bar unless foo # ^^^^^^^^^^^^^^ def visit_unless_node(node) if node.keyword_loc.start_offset == node.location.start_offset builder.condition( token(node.keyword_loc), visit(node.predicate), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.else_clause&.location || node.end_keyword_loc).start_offset) end, visit(node.else_clause), token(node.else_clause&.else_keyword_loc), visit(node.statements), token(node.end_keyword_loc) ) else builder.condition_mod( visit(node.else_clause), visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # until foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar until foo # ^^^^^^^^^^^^^ def visit_until_node(node) if node.location.start_offset == node.keyword_loc.start_offset builder.loop( :until, token(node.keyword_loc), visit(node.predicate), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset) end, visit(node.statements), token(node.closing_loc) ) else builder.loop_mod( :until, visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # case foo; when bar; end # ^^^^^^^^^^^^^ def visit_when_node(node) builder.when( token(node.keyword_loc), visit_all(node.conditions), if (then_keyword_loc = node.then_keyword_loc) token(then_keyword_loc) else srange_semicolon(node.conditions.last.location.end_offset, node.statements&.location&.start_offset) end, visit(node.statements) ) end # while foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar while foo # ^^^^^^^^^^^^^ def visit_while_node(node) if node.location.start_offset == node.keyword_loc.start_offset builder.loop( :while, token(node.keyword_loc), visit(node.predicate), if (do_keyword_loc = node.do_keyword_loc) token(do_keyword_loc) else srange_semicolon(node.predicate.location.end_offset, (node.statements&.location || node.closing_loc).start_offset) end, visit(node.statements), token(node.closing_loc) ) else builder.loop_mod( :while, visit(node.statements), token(node.keyword_loc), visit(node.predicate) ) end end # `foo` # ^^^^^ def visit_x_string_node(node) if node.heredoc? return visit_heredoc(node.to_interpolated) { |children, closing| builder.xstring_compose(token(node.opening_loc), children, closing) } end parts = if node.content == "" [] elsif node.content.include?("\n") string_nodes_from_line_continuations(node.unescaped, node.content, node.content_loc.start_offset, node.opening) else [builder.string_internal([node.unescaped, srange(node.content_loc)])] end builder.xstring_compose( token(node.opening_loc), parts, token(node.closing_loc) ) end # yield # ^^^^^ # # yield 1 # ^^^^^^^ def visit_yield_node(node) builder.keyword_cmd( :yield, token(node.keyword_loc), token(node.lparen_loc), visit(node.arguments) || [], token(node.rparen_loc) ) end private # Initialize a new compiler with the given option overrides, used to # visit a subtree with the given options. def copy_compiler(forwarding: self.forwarding, in_destructure: self.in_destructure, in_pattern: self.in_pattern) Compiler.new(parser, offset_cache, forwarding: forwarding, in_destructure: in_destructure, in_pattern: in_pattern) end # When *, **, &, or ... are used as an argument in a method call, we # check if they were allowed by the current context. To determine that # we build this lookup table. def find_forwarding(node) return [] if node.nil? forwarding = [] forwarding << :* if node.rest.is_a?(RestParameterNode) && node.rest.name.nil? forwarding << :** if node.keyword_rest.is_a?(KeywordRestParameterNode) && node.keyword_rest.name.nil? forwarding << :& if !node.block.nil? && node.block.name.nil? forwarding |= [:&, :"..."] if node.keyword_rest.is_a?(ForwardingParameterNode) forwarding end # Returns the set of targets for a MultiTargetNode or a MultiWriteNode. def multi_target_elements(node) elements = [*node.lefts] elements << node.rest if !node.rest.nil? && !node.rest.is_a?(ImplicitRestNode) elements.concat(node.rights) elements end # Negate the value of a numeric node. This is a special case where you # have a negative sign on one line and then a number on the next line. # In normal Ruby, this will always be a method call. The parser gem, # however, marks this as a numeric literal. We have to massage the tree # here to get it into the correct form. def numeric_negate(message_loc, receiver) case receiver.type when :integer_node, :float_node receiver.copy(value: -receiver.value, location: message_loc.join(receiver.location)) when :rational_node receiver.copy(numerator: -receiver.numerator, location: message_loc.join(receiver.location)) when :imaginary_node receiver.copy(numeric: numeric_negate(message_loc, receiver.numeric), location: message_loc.join(receiver.location)) end end # Blocks can have a special set of parameters that automatically expand # when given arrays if they have a single required parameter and no # other parameters. def procarg0?(parameters) parameters && parameters.requireds.length == 1 && parameters.optionals.empty? && parameters.rest.nil? && parameters.posts.empty? && parameters.keywords.empty? && parameters.keyword_rest.nil? && parameters.block.nil? end # Locations in the parser gem AST are generated using this class. We # store a reference to its constant to make it slightly faster to look # up. Range = ::Parser::Source::Range # Constructs a new source range from the given start and end offsets. def srange(location) Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset]) if location end # Constructs a new source range from the given start and end offsets. def srange_offsets(start_offset, end_offset) Range.new(source_buffer, offset_cache[start_offset], offset_cache[end_offset]) end # Constructs a new source range by finding a semicolon between the given # start offset and end offset. If the semicolon is not found, it returns # nil. Importantly it does not search past newlines or comments. # # Note that end_offset is allowed to be nil, in which case this will # search until the end of the string. def srange_semicolon(start_offset, end_offset) if (match = source_buffer.source.byteslice(start_offset...end_offset)[/\A\s*;/]) final_offset = start_offset + match.bytesize [";", Range.new(source_buffer, offset_cache[final_offset - 1], offset_cache[final_offset])] end end # Transform a location into a token that the parser gem expects. def token(location) [location.slice, Range.new(source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset])] if location end # Visit a block node on a call. def visit_block(call, block) if block parameters = block.parameters implicit_parameters = parameters.is_a?(NumberedParametersNode) || parameters.is_a?(ItParametersNode) builder.block( call, token(block.opening_loc), if parameters.nil? builder.args(nil, [], nil, false) elsif implicit_parameters visit(parameters) else builder.args( token(parameters.opening_loc), if procarg0?(parameters.parameters) parameter = parameters.parameters.requireds.first visited = parameter.is_a?(RequiredParameterNode) ? visit(parameter) : parameter.accept(copy_compiler(in_destructure: true)) [builder.procarg0(visited)].concat(visit_all(parameters.locals)) else visit(parameters) end, token(parameters.closing_loc), false ) end, visit(block.body), token(block.closing_loc) ) else call end end # Visit a heredoc that can be either a string or an xstring. def visit_heredoc(node) children = Array.new indented = false # If this is a dedenting heredoc, then we need to insert the opening # content into the children as well. if node.opening.start_with?("<<~") && node.parts.length > 0 && !node.parts.first.is_a?(StringNode) location = node.parts.first.location location = location.copy(start_offset: location.start_offset - location.start_line_slice.bytesize) children << builder.string_internal(token(location)) indented = true end node.parts.each do |part| pushing = if part.is_a?(StringNode) && part.content.include?("\n") string_nodes_from_line_continuations(part.unescaped, part.content, part.location.start_offset, node.opening) else [visit(part)] end pushing.each do |child| if child.type == :str && child.children.last == "" # nothing elsif child.type == :str && children.last && children.last.type == :str && !children.last.children.first.end_with?("\n") appendee = children[-1] location = appendee.loc location = location.with_expression(location.expression.join(child.loc.expression)) children[-1] = appendee.updated(:str, ["#{appendee.children.first}#{child.children.first}"], location: location) else children << child end end end closing = node.closing closing_t = [closing.chomp, srange_offsets(node.closing_loc.start_offset, node.closing_loc.end_offset - (closing[/\s+$/]&.length || 0))] composed = yield children, closing_t composed = composed.updated(nil, children[1..-1]) if indented composed end # Visit a numeric node and account for the optional sign. def visit_numeric(node, value) if (slice = node.slice).match?(/^[+-]/) builder.unary_num( [slice[0].to_sym, srange_offsets(node.location.start_offset, node.location.start_offset + 1)], value ) else value end end # Within the given block, track that we're within a pattern. def within_pattern begin parser.pattern_variables.push yield copy_compiler(in_pattern: true) ensure parser.pattern_variables.pop end end # When the content of a string node is split across multiple lines, the # parser gem creates individual string nodes for each line the content is part of. def string_nodes_from_interpolation(node, opening) node.parts.flat_map do |part| if part.type == :string_node && part.content.include?("\n") && part.opening_loc.nil? string_nodes_from_line_continuations(part.unescaped, part.content, part.content_loc.start_offset, opening) else visit(part) end end end # Create parser string nodes from a single prism node. The parser gem # "glues" strings together when a line continuation is encountered. def string_nodes_from_line_continuations(unescaped, escaped, start_offset, opening) unescaped = unescaped.lines escaped = escaped.lines percent_array = opening&.start_with?("%w", "%W", "%i", "%I") regex = opening == "/" || opening&.start_with?("%r") # Non-interpolating strings if opening&.end_with?("'") || opening&.start_with?("%q", "%s", "%w", "%i") current_length = 0 current_line = +"" escaped.filter_map.with_index do |escaped_line, index| unescaped_line = unescaped.fetch(index, "") current_length += escaped_line.bytesize current_line << unescaped_line # Glue line continuations together. Only %w and %i arrays can contain these. if percent_array && escaped_line[/(\\)*\n$/, 1]&.length&.odd? next unless index == escaped.count - 1 end s = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_length)]) start_offset += escaped_line.bytesize current_line = +"" current_length = 0 s end else escaped_lengths = [] normalized_lengths = [] # Keeps track of where an unescaped line should start a new token. An unescaped # \n would otherwise be indistinguishable from the actual newline at the end of # of the line. The parser gem only emits a new string node at "real" newlines, # line continuations don't start a new node as well. do_next_tokens = [] escaped .chunk_while { |before, after| before[/(\\*)\r?\n$/, 1]&.length&.odd? || false } .each do |lines| escaped_lengths << lines.sum(&:bytesize) unescaped_lines_count = if regex 0 # Will always be preserved as is else lines.sum do |line| count = line.scan(/(\\*)n/).count { |(backslashes)| backslashes&.length&.odd? } count -= 1 if !line.end_with?("\n") && count > 0 count end end extra = 1 extra = lines.count if percent_array # Account for line continuations in percent arrays normalized_lengths.concat(Array.new(unescaped_lines_count + extra, 0)) normalized_lengths[-1] = lines.sum { |line| line.bytesize } do_next_tokens.concat(Array.new(unescaped_lines_count + extra, false)) do_next_tokens[-1] = true end current_line = +"" current_normalized_length = 0 emitted_count = 0 unescaped.filter_map.with_index do |unescaped_line, index| current_line << unescaped_line current_normalized_length += normalized_lengths.fetch(index, 0) if do_next_tokens[index] inner_part = builder.string_internal([current_line, srange_offsets(start_offset, start_offset + current_normalized_length)]) start_offset += escaped_lengths.fetch(emitted_count, 0) current_line = +"" current_normalized_length = 0 emitted_count += 1 inner_part else nil end end end end end end end end PK!+FU%% ripper.rbnu[# frozen_string_literal: true # :markup: markdown module Prism module Translation # This class provides a compatibility layer between prism and Ripper. It # functions by parsing the entire tree first and then walking it and # executing each of the Ripper callbacks as it goes. To use this class, you # treat `Prism::Translation::Ripper` effectively as you would treat the # `Ripper` class. # # Note that this class will serve the most common use cases, but Ripper's # API is extensive and undocumented. It relies on reporting the state of the # parser at any given time. We do our best to replicate that here, but # because it is a different architecture it is not possible to perfectly # replicate the behavior of Ripper. # # The main known difference is that we may omit dispatching some events in # some cases. This impacts the following events: # # - on_assign_error # - on_comma # - on_ignored_nl # - on_ignored_sp # - on_kw # - on_label_end # - on_lbrace # - on_lbracket # - on_lparen # - on_nl # - on_op # - on_operator_ambiguous # - on_rbrace # - on_rbracket # - on_rparen # - on_semicolon # - on_sp # - on_symbeg # - on_tstring_beg # - on_tstring_end # class Ripper < Compiler # Parses the given Ruby program read from +src+. # +src+ must be a String or an IO or a object with a #gets method. def self.parse(src, filename = "(ripper)", lineno = 1) new(src, filename, lineno).parse end # Tokenizes the Ruby program and returns an array of an array, # which is formatted like # [[lineno, column], type, token, state]. # The +filename+ argument is mostly ignored. # By default, this method does not handle syntax errors in +src+, # use the +raise_errors+ keyword to raise a SyntaxError for an error in +src+. # # require "ripper" # require "pp" # # pp Ripper.lex("def m(a) nil end") # #=> [[[1, 0], :on_kw, "def", FNAME ], # [[1, 3], :on_sp, " ", FNAME ], # [[1, 4], :on_ident, "m", ENDFN ], # [[1, 5], :on_lparen, "(", BEG|LABEL], # [[1, 6], :on_ident, "a", ARG ], # [[1, 7], :on_rparen, ")", ENDFN ], # [[1, 8], :on_sp, " ", BEG ], # [[1, 9], :on_kw, "nil", END ], # [[1, 12], :on_sp, " ", END ], # [[1, 13], :on_kw, "end", END ]] # def self.lex(src, filename = "-", lineno = 1, raise_errors: false) result = Prism.lex_compat(src, filepath: filename, line: lineno, version: "current") if result.failure? && raise_errors raise SyntaxError, result.errors.first.message else result.value end end # This contains a table of all of the parser events and their # corresponding arity. PARSER_EVENT_TABLE = { BEGIN: 1, END: 1, alias: 2, alias_error: 2, aref: 2, aref_field: 2, arg_ambiguous: 1, arg_paren: 1, args_add: 2, args_add_block: 2, args_add_star: 2, args_forward: 0, args_new: 0, array: 1, aryptn: 4, assign: 2, assign_error: 2, assoc_new: 2, assoc_splat: 1, assoclist_from_args: 1, bare_assoc_hash: 1, begin: 1, binary: 3, block_var: 2, blockarg: 1, bodystmt: 4, brace_block: 2, break: 1, call: 3, case: 2, class: 3, class_name_error: 2, command: 2, command_call: 4, const_path_field: 2, const_path_ref: 2, const_ref: 1, def: 3, defined: 1, defs: 5, do_block: 2, dot2: 2, dot3: 2, dyna_symbol: 1, else: 1, elsif: 3, ensure: 1, excessed_comma: 0, fcall: 1, field: 3, fndptn: 4, for: 3, hash: 1, heredoc_dedent: 2, hshptn: 3, if: 3, if_mod: 2, ifop: 3, in: 3, kwrest_param: 1, lambda: 2, magic_comment: 2, massign: 2, method_add_arg: 2, method_add_block: 2, mlhs_add: 2, mlhs_add_post: 2, mlhs_add_star: 2, mlhs_new: 0, mlhs_paren: 1, module: 2, mrhs_add: 2, mrhs_add_star: 2, mrhs_new: 0, mrhs_new_from_args: 1, next: 1, nokw_param: 1, opassign: 3, operator_ambiguous: 2, param_error: 2, params: 7, paren: 1, parse_error: 1, program: 1, qsymbols_add: 2, qsymbols_new: 0, qwords_add: 2, qwords_new: 0, redo: 0, regexp_add: 2, regexp_literal: 2, regexp_new: 0, rescue: 4, rescue_mod: 2, rest_param: 1, retry: 0, return: 1, return0: 0, sclass: 2, stmts_add: 2, stmts_new: 0, string_add: 2, string_concat: 2, string_content: 0, string_dvar: 1, string_embexpr: 1, string_literal: 1, super: 1, symbol: 1, symbol_literal: 1, symbols_add: 2, symbols_new: 0, top_const_field: 1, top_const_ref: 1, unary: 2, undef: 1, unless: 3, unless_mod: 2, until: 2, until_mod: 2, var_alias: 2, var_field: 1, var_ref: 1, vcall: 1, void_stmt: 0, when: 3, while: 2, while_mod: 2, word_add: 2, word_new: 0, words_add: 2, words_new: 0, xstring_add: 2, xstring_literal: 1, xstring_new: 0, yield: 1, yield0: 0, zsuper: 0 } # This contains a table of all of the scanner events and their # corresponding arity. SCANNER_EVENT_TABLE = { CHAR: 1, __end__: 1, backref: 1, backtick: 1, comma: 1, comment: 1, const: 1, cvar: 1, embdoc: 1, embdoc_beg: 1, embdoc_end: 1, embexpr_beg: 1, embexpr_end: 1, embvar: 1, float: 1, gvar: 1, heredoc_beg: 1, heredoc_end: 1, ident: 1, ignored_nl: 1, imaginary: 1, int: 1, ivar: 1, kw: 1, label: 1, label_end: 1, lbrace: 1, lbracket: 1, lparen: 1, nl: 1, op: 1, period: 1, qsymbols_beg: 1, qwords_beg: 1, rational: 1, rbrace: 1, rbracket: 1, regexp_beg: 1, regexp_end: 1, rparen: 1, semicolon: 1, sp: 1, symbeg: 1, symbols_beg: 1, tlambda: 1, tlambeg: 1, tstring_beg: 1, tstring_content: 1, tstring_end: 1, words_beg: 1, words_sep: 1, ignored_sp: 1 } # This array contains name of parser events. PARSER_EVENTS = PARSER_EVENT_TABLE.keys # This array contains name of scanner events. SCANNER_EVENTS = SCANNER_EVENT_TABLE.keys # This array contains name of all ripper events. EVENTS = PARSER_EVENTS + SCANNER_EVENTS # A list of all of the Ruby keywords. KEYWORDS = [ "alias", "and", "begin", "BEGIN", "break", "case", "class", "def", "defined?", "do", "else", "elsif", "end", "END", "ensure", "false", "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", "until", "when", "while", "yield", "__ENCODING__", "__FILE__", "__LINE__" ] # A list of all of the Ruby binary operators. BINARY_OPERATORS = [ :!=, :!~, :=~, :==, :===, :<=>, :>, :>=, :<, :<=, :&, :|, :^, :>>, :<<, :-, :+, :%, :/, :*, :** ] private_constant :KEYWORDS, :BINARY_OPERATORS # Parses +src+ and create S-exp tree. # Returns more readable tree rather than Ripper.sexp_raw. # This method is mainly for developer use. # The +filename+ argument is mostly ignored. # By default, this method does not handle syntax errors in +src+, # returning +nil+ in such cases. Use the +raise_errors+ keyword # to raise a SyntaxError for an error in +src+. # # require "ripper" # require "pp" # # pp Ripper.sexp("def m(a) nil end") # #=> [:program, # [[:def, # [:@ident, "m", [1, 4]], # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil, nil, nil, nil]], # [:bodystmt, [[:var_ref, [:@kw, "nil", [1, 9]]]], nil, nil, nil]]]] # def self.sexp(src, filename = "-", lineno = 1, raise_errors: false) builder = SexpBuilderPP.new(src, filename, lineno) sexp = builder.parse if builder.error? if raise_errors raise SyntaxError, builder.error end else sexp end end # Parses +src+ and create S-exp tree. # This method is mainly for developer use. # The +filename+ argument is mostly ignored. # By default, this method does not handle syntax errors in +src+, # returning +nil+ in such cases. Use the +raise_errors+ keyword # to raise a SyntaxError for an error in +src+. # # require "ripper" # require "pp" # # pp Ripper.sexp_raw("def m(a) nil end") # #=> [:program, # [:stmts_add, # [:stmts_new], # [:def, # [:@ident, "m", [1, 4]], # [:paren, [:params, [[:@ident, "a", [1, 6]]], nil, nil, nil]], # [:bodystmt, # [:stmts_add, [:stmts_new], [:var_ref, [:@kw, "nil", [1, 9]]]], # nil, # nil, # nil]]]] # def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false) builder = SexpBuilder.new(src, filename, lineno) sexp = builder.parse if builder.error? if raise_errors raise SyntaxError, builder.error end else sexp end end autoload :Lexer, "prism/translation/ripper/lexer" autoload :SexpBuilder, "prism/translation/ripper/sexp" autoload :SexpBuilderPP, "prism/translation/ripper/sexp" # :stopdoc: # This is not part of the public API but used by some gems. # Ripper-internal bitflags. LEX_STATE_NAMES = %i[ BEG END ENDARG ENDFN ARG CMDARG MID FNAME DOT CLASS LABEL LABELED FITEM ].map.with_index.to_h { |name, i| [2 ** i, name] }.freeze private_constant :LEX_STATE_NAMES LEX_STATE_NAMES.each do |value, key| const_set("EXPR_#{key}", value) end EXPR_NONE = 0 EXPR_VALUE = EXPR_BEG EXPR_BEG_ANY = EXPR_BEG | EXPR_MID | EXPR_CLASS EXPR_ARG_ANY = EXPR_ARG | EXPR_CMDARG EXPR_END_ANY = EXPR_END | EXPR_ENDARG | EXPR_ENDFN def self.lex_state_name(state) LEX_STATE_NAMES.filter_map { |flag, name| name if state & flag != 0 }.join("|") end # :startdoc: # The source that is being parsed. attr_reader :source # The filename of the source being parsed. attr_reader :filename # The current line number of the parser. attr_reader :lineno # The current column number of the parser. attr_reader :column # Create a new Translation::Ripper object with the given source. def initialize(source, filename = "(ripper)", lineno = 1) @source = source @filename = filename @lineno = lineno @column = 0 @result = nil end ########################################################################## # Public interface ########################################################################## # True if the parser encountered an error during parsing. def error? result.failure? end # Parse the source and return the result. def parse result.comments.each do |comment| location = comment.location bounds(location) if comment.is_a?(InlineComment) on_comment(comment.slice) else offset = location.start_offset lines = comment.slice.lines lines.each_with_index do |line, index| bounds(location.copy(start_offset: offset)) if index == 0 on_embdoc_beg(line) elsif index == lines.size - 1 on_embdoc_end(line) else on_embdoc(line) end offset += line.bytesize end end end result.magic_comments.each do |magic_comment| on_magic_comment(magic_comment.key, magic_comment.value) end unless result.data_loc.nil? on___end__(result.data_loc.slice.each_line.first) end result.warnings.each do |warning| bounds(warning.location) if warning.level == :default warning(warning.message) else case warning.type when :ambiguous_first_argument_plus on_arg_ambiguous("+") when :ambiguous_first_argument_minus on_arg_ambiguous("-") when :ambiguous_slash on_arg_ambiguous("/") else warn(warning.message) end end end if error? result.errors.each do |error| location = error.location bounds(location) case error.type when :alias_argument on_alias_error("can't make alias for the number variables", location.slice) when :argument_formal_class on_param_error("formal argument cannot be a class variable", location.slice) when :argument_format_constant on_param_error("formal argument cannot be a constant", location.slice) when :argument_formal_global on_param_error("formal argument cannot be a global variable", location.slice) when :argument_formal_ivar on_param_error("formal argument cannot be an instance variable", location.slice) when :class_name, :module_name on_class_name_error("class/module name must be CONSTANT", location.slice) else on_parse_error(error.message) end end nil else result.value.accept(self) end end ########################################################################## # Visitor methods ########################################################################## # alias foo bar # ^^^^^^^^^^^^^ def visit_alias_method_node(node) new_name = visit(node.new_name) old_name = visit(node.old_name) bounds(node.location) on_alias(new_name, old_name) end # alias $foo $bar # ^^^^^^^^^^^^^^^ def visit_alias_global_variable_node(node) new_name = visit_alias_global_variable_node_value(node.new_name) old_name = visit_alias_global_variable_node_value(node.old_name) bounds(node.location) on_var_alias(new_name, old_name) end # Visit one side of an alias global variable node. private def visit_alias_global_variable_node_value(node) bounds(node.location) case node when BackReferenceReadNode on_backref(node.slice) when GlobalVariableReadNode on_gvar(node.name.to_s) else raise end end # foo => bar | baz # ^^^^^^^^^ def visit_alternation_pattern_node(node) left = visit_pattern_node(node.left) right = visit_pattern_node(node.right) bounds(node.location) on_binary(left, :|, right) end # Visit a pattern within a pattern match. This is used to bypass the # parenthesis node that can be used to wrap patterns. private def visit_pattern_node(node) if node.is_a?(ParenthesesNode) visit(node.body) else visit(node) end end # a and b # ^^^^^^^ def visit_and_node(node) left = visit(node.left) right = visit(node.right) bounds(node.location) on_binary(left, node.operator.to_sym, right) end # [] # ^^ def visit_array_node(node) case (opening = node.opening) when /^%w/ opening_loc = node.opening_loc bounds(opening_loc) on_qwords_beg(opening) elements = on_qwords_new previous = nil node.elements.each do |element| visit_words_sep(opening_loc, previous, element) bounds(element.location) elements = on_qwords_add(elements, on_tstring_content(element.content)) previous = element end bounds(node.closing_loc) on_tstring_end(node.closing) when /^%i/ opening_loc = node.opening_loc bounds(opening_loc) on_qsymbols_beg(opening) elements = on_qsymbols_new previous = nil node.elements.each do |element| visit_words_sep(opening_loc, previous, element) bounds(element.location) elements = on_qsymbols_add(elements, on_tstring_content(element.value)) previous = element end bounds(node.closing_loc) on_tstring_end(node.closing) when /^%W/ opening_loc = node.opening_loc bounds(opening_loc) on_words_beg(opening) elements = on_words_new previous = nil node.elements.each do |element| visit_words_sep(opening_loc, previous, element) bounds(element.location) elements = on_words_add( elements, if element.is_a?(StringNode) on_word_add(on_word_new, on_tstring_content(element.content)) else element.parts.inject(on_word_new) do |word, part| word_part = if part.is_a?(StringNode) bounds(part.location) on_tstring_content(part.content) else visit(part) end on_word_add(word, word_part) end end ) previous = element end bounds(node.closing_loc) on_tstring_end(node.closing) when /^%I/ opening_loc = node.opening_loc bounds(opening_loc) on_symbols_beg(opening) elements = on_symbols_new previous = nil node.elements.each do |element| visit_words_sep(opening_loc, previous, element) bounds(element.location) elements = on_symbols_add( elements, if element.is_a?(SymbolNode) on_word_add(on_word_new, on_tstring_content(element.value)) else element.parts.inject(on_word_new) do |word, part| word_part = if part.is_a?(StringNode) bounds(part.location) on_tstring_content(part.content) else visit(part) end on_word_add(word, word_part) end end ) previous = element end bounds(node.closing_loc) on_tstring_end(node.closing) else bounds(node.opening_loc) on_lbracket(opening) elements = visit_arguments(node.elements) unless node.elements.empty? bounds(node.closing_loc) on_rbracket(node.closing) end bounds(node.location) on_array(elements) end # Dispatch a words_sep event that contains the space between the elements # of list literals. private def visit_words_sep(opening_loc, previous, current) end_offset = (previous.nil? ? opening_loc : previous.location).end_offset start_offset = current.location.start_offset if end_offset != start_offset bounds(current.location.copy(start_offset: end_offset)) on_words_sep(source.byteslice(end_offset...start_offset)) end end # Visit a list of elements, like the elements of an array or arguments. private def visit_arguments(elements) bounds(elements.first.location) elements.inject(on_args_new) do |args, element| arg = visit(element) bounds(element.location) case element when BlockArgumentNode on_args_add_block(args, arg) when SplatNode on_args_add_star(args, arg) else on_args_add(args, arg) end end end # foo => [bar] # ^^^^^ def visit_array_pattern_node(node) constant = visit(node.constant) requireds = visit_all(node.requireds) if node.requireds.any? rest = if (rest_node = node.rest).is_a?(SplatNode) if rest_node.expression.nil? bounds(rest_node.location) on_var_field(nil) else visit(rest_node.expression) end end posts = visit_all(node.posts) if node.posts.any? bounds(node.location) on_aryptn(constant, requireds, rest, posts) end # foo(bar) # ^^^ def visit_arguments_node(node) arguments, _ = visit_call_node_arguments(node, nil, false) arguments end # { a: 1 } # ^^^^ def visit_assoc_node(node) key = visit(node.key) value = visit(node.value) bounds(node.location) on_assoc_new(key, value) end # def foo(**); bar(**); end # ^^ # # { **foo } # ^^^^^ def visit_assoc_splat_node(node) value = visit(node.value) bounds(node.location) on_assoc_splat(value) end # $+ # ^^ def visit_back_reference_read_node(node) bounds(node.location) on_backref(node.slice) end # begin end # ^^^^^^^^^ def visit_begin_node(node) clauses = visit_begin_node_clauses(node.begin_keyword_loc, node, false) bounds(node.location) on_begin(clauses) end # Visit the clauses of a begin node to form an on_bodystmt call. private def visit_begin_node_clauses(location, node, allow_newline) statements = if node.statements.nil? on_stmts_add(on_stmts_new, on_void_stmt) else body = node.statements.body body.unshift(nil) if void_stmt?(location, node.statements.body[0].location, allow_newline) bounds(node.statements.location) visit_statements_node_body(body) end rescue_clause = visit(node.rescue_clause) else_clause = unless (else_clause_node = node.else_clause).nil? else_statements = if else_clause_node.statements.nil? [nil] else body = else_clause_node.statements.body body.unshift(nil) if void_stmt?(else_clause_node.else_keyword_loc, else_clause_node.statements.body[0].location, allow_newline) body end bounds(else_clause_node.location) visit_statements_node_body(else_statements) end ensure_clause = visit(node.ensure_clause) bounds(node.location) on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) end # Visit the body of a structure that can have either a set of statements # or statements wrapped in rescue/else/ensure. private def visit_body_node(location, node, allow_newline = false) case node when nil bounds(location) on_bodystmt(visit_statements_node_body([nil]), nil, nil, nil) when StatementsNode body = [*node.body] body.unshift(nil) if void_stmt?(location, body[0].location, allow_newline) stmts = visit_statements_node_body(body) bounds(node.body.first.location) on_bodystmt(stmts, nil, nil, nil) when BeginNode visit_begin_node_clauses(location, node, allow_newline) else raise end end # foo(&bar) # ^^^^ def visit_block_argument_node(node) visit(node.expression) end # foo { |; bar| } # ^^^ def visit_block_local_variable_node(node) bounds(node.location) on_ident(node.name.to_s) end # Visit a BlockNode. def visit_block_node(node) braces = node.opening == "{" parameters = visit(node.parameters) body = case node.body when nil bounds(node.location) stmts = on_stmts_add(on_stmts_new, on_void_stmt) bounds(node.location) braces ? stmts : on_bodystmt(stmts, nil, nil, nil) when StatementsNode stmts = node.body.body stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false) stmts = visit_statements_node_body(stmts) bounds(node.body.location) braces ? stmts : on_bodystmt(stmts, nil, nil, nil) when BeginNode visit_body_node(node.parameters&.location || node.opening_loc, node.body) else raise end if braces bounds(node.location) on_brace_block(parameters, body) else bounds(node.location) on_do_block(parameters, body) end end # def foo(&bar); end # ^^^^ def visit_block_parameter_node(node) if node.name_loc.nil? bounds(node.location) on_blockarg(nil) else bounds(node.name_loc) name = visit_token(node.name.to_s) bounds(node.location) on_blockarg(name) end end # A block's parameters. def visit_block_parameters_node(node) parameters = if node.parameters.nil? on_params(nil, nil, nil, nil, nil, nil, nil) else visit(node.parameters) end locals = if node.locals.any? visit_all(node.locals) else false end bounds(node.location) on_block_var(parameters, locals) end # break # ^^^^^ # # break foo # ^^^^^^^^^ def visit_break_node(node) if node.arguments.nil? bounds(node.location) on_break(on_args_new) else arguments = visit(node.arguments) bounds(node.location) on_break(arguments) end end # foo # ^^^ # # foo.bar # ^^^^^^^ # # foo.bar() {} # ^^^^^^^^^^^^ def visit_call_node(node) if node.call_operator_loc.nil? case node.name when :[] receiver = visit(node.receiver) arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) call = on_aref(receiver, arguments) if block.nil? call else bounds(node.location) on_method_add_block(call, block) end when :[]= receiver = visit(node.receiver) *arguments, last_argument = node.arguments.arguments arguments << node.block if !node.block.nil? arguments = if arguments.any? args = visit_arguments(arguments) if !node.block.nil? args else bounds(arguments.first.location) on_args_add_block(args, false) end end bounds(node.location) call = on_aref_field(receiver, arguments) value = visit_write_value(last_argument) bounds(last_argument.location) on_assign(call, value) when :-@, :+@, :~ receiver = visit(node.receiver) bounds(node.location) on_unary(node.name, receiver) when :! if node.message == "not" receiver = if !node.receiver.is_a?(ParenthesesNode) || !node.receiver.body.nil? visit(node.receiver) end bounds(node.location) on_unary(:not, receiver) else receiver = visit(node.receiver) bounds(node.location) on_unary(:!, receiver) end when *BINARY_OPERATORS receiver = visit(node.receiver) value = visit(node.arguments.arguments.first) bounds(node.location) on_binary(receiver, node.name, value) else bounds(node.message_loc) message = visit_token(node.message, false) if node.variable_call? on_vcall(message) else arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) call = if node.opening_loc.nil? && arguments&.any? bounds(node.location) on_command(message, arguments) elsif !node.opening_loc.nil? bounds(node.location) on_method_add_arg(on_fcall(message), on_arg_paren(arguments)) else bounds(node.location) on_method_add_arg(on_fcall(message), on_args_new) end if block.nil? call else bounds(node.block.location) on_method_add_block(call, block) end end end else receiver = visit(node.receiver) bounds(node.call_operator_loc) call_operator = visit_token(node.call_operator) message = if node.message_loc.nil? :call else bounds(node.message_loc) visit_token(node.message, false) end if node.name.end_with?("=") && !node.message.end_with?("=") && !node.arguments.nil? && node.block.nil? value = visit_write_value(node.arguments.arguments.first) bounds(node.location) on_assign(on_field(receiver, call_operator, message), value) else arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc || node.location)) call = if node.opening_loc.nil? bounds(node.location) if node.arguments.nil? && !node.block.is_a?(BlockArgumentNode) on_call(receiver, call_operator, message) else on_command_call(receiver, call_operator, message, arguments) end else bounds(node.opening_loc) arguments = on_arg_paren(arguments) bounds(node.location) on_method_add_arg(on_call(receiver, call_operator, message), arguments) end if block.nil? call else bounds(node.block.location) on_method_add_block(call, block) end end end end # Visit the arguments and block of a call node and return the arguments # and block as they should be used. private def visit_call_node_arguments(arguments_node, block_node, trailing_comma) arguments = arguments_node&.arguments || [] block = block_node if block.is_a?(BlockArgumentNode) arguments << block block = nil end [ if arguments.length == 1 && arguments.first.is_a?(ForwardingArgumentsNode) visit(arguments.first) elsif arguments.any? args = visit_arguments(arguments) if block_node.is_a?(BlockArgumentNode) || arguments.last.is_a?(ForwardingArgumentsNode) || command?(arguments.last) || trailing_comma args else bounds(arguments.first.location) on_args_add_block(args, false) end end, visit(block) ] end # Returns true if the given node is a command node. private def command?(node) node.is_a?(CallNode) && node.opening_loc.nil? && (!node.arguments.nil? || node.block.is_a?(BlockArgumentNode)) && !BINARY_OPERATORS.include?(node.name) end # foo.bar += baz # ^^^^^^^^^^^^^^^ def visit_call_operator_write_node(node) receiver = visit(node.receiver) bounds(node.call_operator_loc) call_operator = visit_token(node.call_operator) bounds(node.message_loc) message = visit_token(node.message) bounds(node.location) target = on_field(receiver, call_operator, message) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo.bar &&= baz # ^^^^^^^^^^^^^^^ def visit_call_and_write_node(node) receiver = visit(node.receiver) bounds(node.call_operator_loc) call_operator = visit_token(node.call_operator) bounds(node.message_loc) message = visit_token(node.message) bounds(node.location) target = on_field(receiver, call_operator, message) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo.bar ||= baz # ^^^^^^^^^^^^^^^ def visit_call_or_write_node(node) receiver = visit(node.receiver) bounds(node.call_operator_loc) call_operator = visit_token(node.call_operator) bounds(node.message_loc) message = visit_token(node.message) bounds(node.location) target = on_field(receiver, call_operator, message) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo.bar, = 1 # ^^^^^^^ def visit_call_target_node(node) if node.call_operator == "::" receiver = visit(node.receiver) bounds(node.message_loc) message = visit_token(node.message) bounds(node.location) on_const_path_field(receiver, message) else receiver = visit(node.receiver) bounds(node.call_operator_loc) call_operator = visit_token(node.call_operator) bounds(node.message_loc) message = visit_token(node.message) bounds(node.location) on_field(receiver, call_operator, message) end end # foo => bar => baz # ^^^^^^^^^^ def visit_capture_pattern_node(node) value = visit(node.value) target = visit(node.target) bounds(node.location) on_binary(value, :"=>", target) end # case foo; when bar; end # ^^^^^^^^^^^^^^^^^^^^^^^ def visit_case_node(node) predicate = visit(node.predicate) clauses = node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition| on_when(*visit(condition), current) end bounds(node.location) on_case(predicate, clauses) end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_case_match_node(node) predicate = visit(node.predicate) clauses = node.conditions.reverse_each.inject(visit(node.else_clause)) do |current, condition| on_in(*visit(condition), current) end bounds(node.location) on_case(predicate, clauses) end # class Foo; end # ^^^^^^^^^^^^^^ def visit_class_node(node) constant_path = if node.constant_path.is_a?(ConstantReadNode) bounds(node.constant_path.location) on_const_ref(on_const(node.constant_path.name.to_s)) else visit(node.constant_path) end superclass = visit(node.superclass) bodystmt = visit_body_node(node.superclass&.location || node.constant_path.location, node.body, node.superclass.nil?) bounds(node.location) on_class(constant_path, superclass, bodystmt) end # @@foo # ^^^^^ def visit_class_variable_read_node(node) bounds(node.location) on_var_ref(on_cvar(node.slice)) end # @@foo = 1 # ^^^^^^^^^ # # @@foo, @@bar = 1 # ^^^^^ ^^^^^ def visit_class_variable_write_node(node) bounds(node.name_loc) target = on_var_field(on_cvar(node.name.to_s)) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # @@foo += bar # ^^^^^^^^^^^^ def visit_class_variable_operator_write_node(node) bounds(node.name_loc) target = on_var_field(on_cvar(node.name.to_s)) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @@foo &&= bar # ^^^^^^^^^^^^^ def visit_class_variable_and_write_node(node) bounds(node.name_loc) target = on_var_field(on_cvar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @@foo ||= bar # ^^^^^^^^^^^^^ def visit_class_variable_or_write_node(node) bounds(node.name_loc) target = on_var_field(on_cvar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @@foo, = bar # ^^^^^ def visit_class_variable_target_node(node) bounds(node.location) on_var_field(on_cvar(node.name.to_s)) end # Foo # ^^^ def visit_constant_read_node(node) bounds(node.location) on_var_ref(on_const(node.name.to_s)) end # Foo = 1 # ^^^^^^^ # # Foo, Bar = 1 # ^^^ ^^^ def visit_constant_write_node(node) bounds(node.name_loc) target = on_var_field(on_const(node.name.to_s)) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # Foo += bar # ^^^^^^^^^^^ def visit_constant_operator_write_node(node) bounds(node.name_loc) target = on_var_field(on_const(node.name.to_s)) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo &&= bar # ^^^^^^^^^^^^ def visit_constant_and_write_node(node) bounds(node.name_loc) target = on_var_field(on_const(node.name.to_s)) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo ||= bar # ^^^^^^^^^^^^ def visit_constant_or_write_node(node) bounds(node.name_loc) target = on_var_field(on_const(node.name.to_s)) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo, = bar # ^^^ def visit_constant_target_node(node) bounds(node.location) on_var_field(on_const(node.name.to_s)) end # Foo::Bar # ^^^^^^^^ def visit_constant_path_node(node) if node.parent.nil? bounds(node.name_loc) child = on_const(node.name.to_s) bounds(node.location) on_top_const_ref(child) else parent = visit(node.parent) bounds(node.name_loc) child = on_const(node.name.to_s) bounds(node.location) on_const_path_ref(parent, child) end end # Foo::Bar = 1 # ^^^^^^^^^^^^ # # Foo::Foo, Bar::Bar = 1 # ^^^^^^^^ ^^^^^^^^ def visit_constant_path_write_node(node) target = visit_constant_path_write_node_target(node.target) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # Visit a constant path that is part of a write node. private def visit_constant_path_write_node_target(node) if node.parent.nil? bounds(node.name_loc) child = on_const(node.name.to_s) bounds(node.location) on_top_const_field(child) else parent = visit(node.parent) bounds(node.name_loc) child = on_const(node.name.to_s) bounds(node.location) on_const_path_field(parent, child) end end # Foo::Bar += baz # ^^^^^^^^^^^^^^^ def visit_constant_path_operator_write_node(node) target = visit_constant_path_write_node_target(node.target) value = visit(node.value) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo::Bar &&= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_and_write_node(node) target = visit_constant_path_write_node_target(node.target) value = visit(node.value) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo::Bar ||= baz # ^^^^^^^^^^^^^^^^ def visit_constant_path_or_write_node(node) target = visit_constant_path_write_node_target(node.target) value = visit(node.value) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # Foo::Bar, = baz # ^^^^^^^^ def visit_constant_path_target_node(node) visit_constant_path_write_node_target(node) end # def foo; end # ^^^^^^^^^^^^ # # def self.foo; end # ^^^^^^^^^^^^^^^^^ def visit_def_node(node) receiver = visit(node.receiver) operator = if !node.operator_loc.nil? bounds(node.operator_loc) visit_token(node.operator) end bounds(node.name_loc) name = visit_token(node.name_loc.slice) parameters = if node.parameters.nil? bounds(node.location) on_params(nil, nil, nil, nil, nil, nil, nil) else visit(node.parameters) end if !node.lparen_loc.nil? bounds(node.lparen_loc) parameters = on_paren(parameters) end bodystmt = if node.equal_loc.nil? visit_body_node(node.rparen_loc || node.end_keyword_loc, node.body) else body = visit(node.body.body.first) bounds(node.body.location) on_bodystmt(body, nil, nil, nil) end bounds(node.location) if receiver.nil? on_def(name, parameters, bodystmt) else on_defs(receiver, operator, name, parameters, bodystmt) end end # defined? a # ^^^^^^^^^^ # # defined?(a) # ^^^^^^^^^^^ def visit_defined_node(node) expression = visit(node.value) # Very weird circumstances here where something like: # # defined? # (1) # # gets parsed in Ruby as having only the `1` expression but in Ripper it # gets parsed as having a parentheses node. In this case we need to # synthesize that node to match Ripper's behavior. if node.lparen_loc && node.keyword_loc.join(node.lparen_loc).slice.include?("\n") bounds(node.lparen_loc.join(node.rparen_loc)) expression = on_paren(on_stmts_add(on_stmts_new, expression)) end bounds(node.location) on_defined(expression) end # if foo then bar else baz end # ^^^^^^^^^^^^ def visit_else_node(node) statements = if node.statements.nil? [nil] else body = node.statements.body body.unshift(nil) if void_stmt?(node.else_keyword_loc, node.statements.body[0].location, false) body end bounds(node.location) on_else(visit_statements_node_body(statements)) end # "foo #{bar}" # ^^^^^^ def visit_embedded_statements_node(node) bounds(node.opening_loc) on_embexpr_beg(node.opening) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.closing_loc) on_embexpr_end(node.closing) bounds(node.location) on_string_embexpr(statements) end # "foo #@bar" # ^^^^^ def visit_embedded_variable_node(node) bounds(node.operator_loc) on_embvar(node.operator) variable = visit(node.variable) bounds(node.location) on_string_dvar(variable) end # Visit an EnsureNode node. def visit_ensure_node(node) statements = if node.statements.nil? [nil] else body = node.statements.body body.unshift(nil) if void_stmt?(node.ensure_keyword_loc, body[0].location, false) body end statements = visit_statements_node_body(statements) bounds(node.location) on_ensure(statements) end # false # ^^^^^ def visit_false_node(node) bounds(node.location) on_var_ref(on_kw("false")) end # foo => [*, bar, *] # ^^^^^^^^^^^ def visit_find_pattern_node(node) constant = visit(node.constant) left = if node.left.expression.nil? bounds(node.left.location) on_var_field(nil) else visit(node.left.expression) end requireds = visit_all(node.requireds) if node.requireds.any? right = if node.right.expression.nil? bounds(node.right.location) on_var_field(nil) else visit(node.right.expression) end bounds(node.location) on_fndptn(constant, left, requireds, right) end # if foo .. bar; end # ^^^^^^^^^^ def visit_flip_flop_node(node) left = visit(node.left) right = visit(node.right) bounds(node.location) if node.exclude_end? on_dot3(left, right) else on_dot2(left, right) end end # 1.0 # ^^^ def visit_float_node(node) visit_number_node(node) { |text| on_float(text) } end # for foo in bar do end # ^^^^^^^^^^^^^^^^^^^^^ def visit_for_node(node) index = visit(node.index) collection = visit(node.collection) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.location) on_for(index, collection, statements) end # def foo(...); bar(...); end # ^^^ def visit_forwarding_arguments_node(node) bounds(node.location) on_args_forward end # def foo(...); end # ^^^ def visit_forwarding_parameter_node(node) bounds(node.location) on_args_forward end # super # ^^^^^ # # super {} # ^^^^^^^^ def visit_forwarding_super_node(node) if node.block.nil? bounds(node.location) on_zsuper else block = visit(node.block) bounds(node.location) on_method_add_block(on_zsuper, block) end end # $foo # ^^^^ def visit_global_variable_read_node(node) bounds(node.location) on_var_ref(on_gvar(node.name.to_s)) end # $foo = 1 # ^^^^^^^^ # # $foo, $bar = 1 # ^^^^ ^^^^ def visit_global_variable_write_node(node) bounds(node.name_loc) target = on_var_field(on_gvar(node.name.to_s)) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # $foo += bar # ^^^^^^^^^^^ def visit_global_variable_operator_write_node(node) bounds(node.name_loc) target = on_var_field(on_gvar(node.name.to_s)) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # $foo &&= bar # ^^^^^^^^^^^^ def visit_global_variable_and_write_node(node) bounds(node.name_loc) target = on_var_field(on_gvar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # $foo ||= bar # ^^^^^^^^^^^^ def visit_global_variable_or_write_node(node) bounds(node.name_loc) target = on_var_field(on_gvar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # $foo, = bar # ^^^^ def visit_global_variable_target_node(node) bounds(node.location) on_var_field(on_gvar(node.name.to_s)) end # {} # ^^ def visit_hash_node(node) elements = if node.elements.any? args = visit_all(node.elements) bounds(node.elements.first.location) on_assoclist_from_args(args) end bounds(node.location) on_hash(elements) end # foo => {} # ^^ def visit_hash_pattern_node(node) constant = visit(node.constant) elements = if node.elements.any? || !node.rest.nil? node.elements.map do |element| [ if (key = element.key).opening_loc.nil? visit(key) else bounds(key.value_loc) if (value = key.value).empty? on_string_content else on_string_add(on_string_content, on_tstring_content(value)) end end, visit(element.value) ] end end rest = case node.rest when AssocSplatNode visit(node.rest.value) when NoKeywordsParameterNode bounds(node.rest.location) on_var_field(visit(node.rest)) end bounds(node.location) on_hshptn(constant, elements, rest) end # if foo then bar end # ^^^^^^^^^^^^^^^^^^^ # # bar if foo # ^^^^^^^^^^ # # foo ? bar : baz # ^^^^^^^^^^^^^^^ def visit_if_node(node) if node.then_keyword == "?" predicate = visit(node.predicate) truthy = visit(node.statements.body.first) falsy = visit(node.subsequent.statements.body.first) bounds(node.location) on_ifop(predicate, truthy, falsy) elsif node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset) predicate = visit(node.predicate) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end subsequent = visit(node.subsequent) bounds(node.location) if node.if_keyword == "if" on_if(predicate, statements, subsequent) else on_elsif(predicate, statements, subsequent) end else statements = visit(node.statements.body.first) predicate = visit(node.predicate) bounds(node.location) on_if_mod(predicate, statements) end end # 1i # ^^ def visit_imaginary_node(node) visit_number_node(node) { |text| on_imaginary(text) } end # { foo: } # ^^^^ def visit_implicit_node(node) end # foo { |bar,| } # ^ def visit_implicit_rest_node(node) bounds(node.location) on_excessed_comma end # case foo; in bar; end # ^^^^^^^^^^^^^^^^^^^^^ def visit_in_node(node) # This is a special case where we're not going to call on_in directly # because we don't have access to the subsequent. Instead, we'll return # the component parts and let the parent node handle it. pattern = visit_pattern_node(node.pattern) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end [pattern, statements] end # foo[bar] += baz # ^^^^^^^^^^^^^^^ def visit_index_operator_write_node(node) receiver = visit(node.receiver) arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo[bar] &&= baz # ^^^^^^^^^^^^^^^^ def visit_index_and_write_node(node) receiver = visit(node.receiver) arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo[bar] ||= baz # ^^^^^^^^^^^^^^^^ def visit_index_or_write_node(node) receiver = visit(node.receiver) arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) target = on_aref_field(receiver, arguments) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo[bar], = 1 # ^^^^^^^^ def visit_index_target_node(node) receiver = visit(node.receiver) arguments, _ = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.closing_loc)) bounds(node.location) on_aref_field(receiver, arguments) end # @foo # ^^^^ def visit_instance_variable_read_node(node) bounds(node.location) on_var_ref(on_ivar(node.name.to_s)) end # @foo = 1 # ^^^^^^^^ def visit_instance_variable_write_node(node) bounds(node.name_loc) target = on_var_field(on_ivar(node.name.to_s)) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # @foo += bar # ^^^^^^^^^^^ def visit_instance_variable_operator_write_node(node) bounds(node.name_loc) target = on_var_field(on_ivar(node.name.to_s)) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @foo &&= bar # ^^^^^^^^^^^^ def visit_instance_variable_and_write_node(node) bounds(node.name_loc) target = on_var_field(on_ivar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @foo ||= bar # ^^^^^^^^^^^^ def visit_instance_variable_or_write_node(node) bounds(node.name_loc) target = on_var_field(on_ivar(node.name.to_s)) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # @foo, = bar # ^^^^ def visit_instance_variable_target_node(node) bounds(node.location) on_var_field(on_ivar(node.name.to_s)) end # 1 # ^ def visit_integer_node(node) visit_number_node(node) { |text| on_int(text) } end # if /foo #{bar}/ then end # ^^^^^^^^^^^^ def visit_interpolated_match_last_line_node(node) bounds(node.opening_loc) on_regexp_beg(node.opening) bounds(node.parts.first.location) parts = node.parts.inject(on_regexp_new) do |content, part| on_regexp_add(content, visit_string_content(part)) end bounds(node.closing_loc) closing = on_regexp_end(node.closing) bounds(node.location) on_regexp_literal(parts, closing) end # /foo #{bar}/ # ^^^^^^^^^^^^ def visit_interpolated_regular_expression_node(node) bounds(node.opening_loc) on_regexp_beg(node.opening) bounds(node.parts.first.location) parts = node.parts.inject(on_regexp_new) do |content, part| on_regexp_add(content, visit_string_content(part)) end bounds(node.closing_loc) closing = on_regexp_end(node.closing) bounds(node.location) on_regexp_literal(parts, closing) end # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) if node.opening&.start_with?("<<~") heredoc = visit_heredoc_string_node(node) bounds(node.location) on_string_literal(heredoc) elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? } first, *rest = node.parts rest.inject(visit(first)) do |content, part| concat = visit(part) bounds(part.location) on_string_concat(content, concat) end else bounds(node.parts.first.location) parts = node.parts.inject(on_string_content) do |content, part| on_string_add(content, visit_string_content(part)) end bounds(node.location) on_string_literal(parts) end end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) bounds(node.parts.first.location) parts = node.parts.inject(on_string_content) do |content, part| on_string_add(content, visit_string_content(part)) end bounds(node.location) on_dyna_symbol(parts) end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) if node.opening.start_with?("<<~") heredoc = visit_heredoc_x_string_node(node) bounds(node.location) on_xstring_literal(heredoc) else bounds(node.parts.first.location) parts = node.parts.inject(on_xstring_new) do |content, part| on_xstring_add(content, visit_string_content(part)) end bounds(node.location) on_xstring_literal(parts) end end # Visit an individual part of a string-like node. private def visit_string_content(part) if part.is_a?(StringNode) bounds(part.content_loc) on_tstring_content(part.content) else visit(part) end end # -> { it } # ^^ def visit_it_local_variable_read_node(node) bounds(node.location) on_vcall(on_ident(node.slice)) end # -> { it } # ^^^^^^^^^ def visit_it_parameters_node(node) end # foo(bar: baz) # ^^^^^^^^ def visit_keyword_hash_node(node) elements = visit_all(node.elements) bounds(node.location) on_bare_assoc_hash(elements) end # def foo(**bar); end # ^^^^^ # # def foo(**); end # ^^ def visit_keyword_rest_parameter_node(node) if node.name_loc.nil? bounds(node.location) on_kwrest_param(nil) else bounds(node.name_loc) name = on_ident(node.name.to_s) bounds(node.location) on_kwrest_param(name) end end # -> {} def visit_lambda_node(node) bounds(node.operator_loc) on_tlambda(node.operator) parameters = if node.parameters.is_a?(BlockParametersNode) # Ripper does not track block-locals within lambdas, so we skip # directly to the parameters here. params = if node.parameters.parameters.nil? bounds(node.location) on_params(nil, nil, nil, nil, nil, nil, nil) else visit(node.parameters.parameters) end if node.parameters.opening_loc.nil? params else bounds(node.parameters.opening_loc) on_paren(params) end else bounds(node.location) on_params(nil, nil, nil, nil, nil, nil, nil) end braces = node.opening == "{" if braces bounds(node.opening_loc) on_tlambeg(node.opening) end body = case node.body when nil bounds(node.location) stmts = on_stmts_add(on_stmts_new, on_void_stmt) bounds(node.location) braces ? stmts : on_bodystmt(stmts, nil, nil, nil) when StatementsNode stmts = node.body.body stmts.unshift(nil) if void_stmt?(node.parameters&.location || node.opening_loc, node.body.location, false) stmts = visit_statements_node_body(stmts) bounds(node.body.location) braces ? stmts : on_bodystmt(stmts, nil, nil, nil) when BeginNode visit_body_node(node.opening_loc, node.body) else raise end bounds(node.location) on_lambda(parameters, body) end # foo # ^^^ def visit_local_variable_read_node(node) bounds(node.location) on_var_ref(on_ident(node.slice)) end # foo = 1 # ^^^^^^^ def visit_local_variable_write_node(node) bounds(node.name_loc) target = on_var_field(on_ident(node.name_loc.slice)) value = visit_write_value(node.value) bounds(node.location) on_assign(target, value) end # foo += bar # ^^^^^^^^^^ def visit_local_variable_operator_write_node(node) bounds(node.name_loc) target = on_var_field(on_ident(node.name_loc.slice)) bounds(node.binary_operator_loc) operator = on_op("#{node.binary_operator}=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo &&= bar # ^^^^^^^^^^^ def visit_local_variable_and_write_node(node) bounds(node.name_loc) target = on_var_field(on_ident(node.name_loc.slice)) bounds(node.operator_loc) operator = on_op("&&=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo ||= bar # ^^^^^^^^^^^ def visit_local_variable_or_write_node(node) bounds(node.name_loc) target = on_var_field(on_ident(node.name_loc.slice)) bounds(node.operator_loc) operator = on_op("||=") value = visit_write_value(node.value) bounds(node.location) on_opassign(target, operator, value) end # foo, = bar # ^^^ def visit_local_variable_target_node(node) bounds(node.location) on_var_field(on_ident(node.name.to_s)) end # if /foo/ then end # ^^^^^ def visit_match_last_line_node(node) bounds(node.opening_loc) on_regexp_beg(node.opening) bounds(node.content_loc) tstring_content = on_tstring_content(node.content) bounds(node.closing_loc) closing = on_regexp_end(node.closing) on_regexp_literal(on_regexp_add(on_regexp_new, tstring_content), closing) end # foo in bar # ^^^^^^^^^^ def visit_match_predicate_node(node) value = visit(node.value) pattern = on_in(visit_pattern_node(node.pattern), nil, nil) on_case(value, pattern) end # foo => bar # ^^^^^^^^^^ def visit_match_required_node(node) value = visit(node.value) pattern = on_in(visit_pattern_node(node.pattern), nil, nil) on_case(value, pattern) end # /(?foo)/ =~ bar # ^^^^^^^^^^^^^^^^^^^^ def visit_match_write_node(node) visit(node.call) end # A node that is missing from the syntax tree. This is only used in the # case of a syntax error. def visit_missing_node(node) raise "Cannot visit missing nodes directly." end # module Foo; end # ^^^^^^^^^^^^^^^ def visit_module_node(node) constant_path = if node.constant_path.is_a?(ConstantReadNode) bounds(node.constant_path.location) on_const_ref(on_const(node.constant_path.name.to_s)) else visit(node.constant_path) end bodystmt = visit_body_node(node.constant_path.location, node.body, true) bounds(node.location) on_module(constant_path, bodystmt) end # (foo, bar), bar = qux # ^^^^^^^^^^ def visit_multi_target_node(node) bounds(node.location) targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, true) if node.lparen_loc.nil? targets else bounds(node.lparen_loc) on_mlhs_paren(targets) end end # Visit the targets of a multi-target node. private def visit_multi_target_node_targets(lefts, rest, rights, skippable) if skippable && lefts.length == 1 && lefts.first.is_a?(MultiTargetNode) && rest.nil? && rights.empty? return visit(lefts.first) end mlhs = on_mlhs_new lefts.each do |left| bounds(left.location) mlhs = on_mlhs_add(mlhs, visit(left)) end case rest when nil # do nothing when ImplicitRestNode # these do not get put into the generated tree bounds(rest.location) on_excessed_comma else bounds(rest.location) mlhs = on_mlhs_add_star(mlhs, visit(rest)) end if rights.any? bounds(rights.first.location) post = on_mlhs_new rights.each do |right| bounds(right.location) post = on_mlhs_add(post, visit(right)) end mlhs = on_mlhs_add_post(mlhs, post) end mlhs end # foo, bar = baz # ^^^^^^^^^^^^^^ def visit_multi_write_node(node) bounds(node.location) targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, true) unless node.lparen_loc.nil? bounds(node.lparen_loc) targets = on_mlhs_paren(targets) end value = visit_write_value(node.value) bounds(node.location) on_massign(targets, value) end # next # ^^^^ # # next foo # ^^^^^^^^ def visit_next_node(node) if node.arguments.nil? bounds(node.location) on_next(on_args_new) else arguments = visit(node.arguments) bounds(node.location) on_next(arguments) end end # nil # ^^^ def visit_nil_node(node) bounds(node.location) on_var_ref(on_kw("nil")) end # def foo(**nil); end # ^^^^^ def visit_no_keywords_parameter_node(node) bounds(node.location) on_nokw_param(nil) :nil end # -> { _1 + _2 } # ^^^^^^^^^^^^^^ def visit_numbered_parameters_node(node) end # $1 # ^^ def visit_numbered_reference_read_node(node) bounds(node.location) on_backref(node.slice) end # def foo(bar: baz); end # ^^^^^^^^ def visit_optional_keyword_parameter_node(node) bounds(node.name_loc) name = on_label("#{node.name}:") value = visit(node.value) [name, value] end # def foo(bar = 1); end # ^^^^^^^ def visit_optional_parameter_node(node) bounds(node.name_loc) name = visit_token(node.name.to_s) value = visit(node.value) [name, value] end # a or b # ^^^^^^ def visit_or_node(node) left = visit(node.left) right = visit(node.right) bounds(node.location) on_binary(left, node.operator.to_sym, right) end # def foo(bar, *baz); end # ^^^^^^^^^ def visit_parameters_node(node) requireds = node.requireds.map { |required| required.is_a?(MultiTargetNode) ? visit_destructured_parameter_node(required) : visit(required) } if node.requireds.any? optionals = visit_all(node.optionals) if node.optionals.any? rest = visit(node.rest) posts = node.posts.map { |post| post.is_a?(MultiTargetNode) ? visit_destructured_parameter_node(post) : visit(post) } if node.posts.any? keywords = visit_all(node.keywords) if node.keywords.any? keyword_rest = visit(node.keyword_rest) block = visit(node.block) bounds(node.location) on_params(requireds, optionals, rest, posts, keywords, keyword_rest, block) end # Visit a destructured positional parameter node. private def visit_destructured_parameter_node(node) bounds(node.location) targets = visit_multi_target_node_targets(node.lefts, node.rest, node.rights, false) bounds(node.lparen_loc) on_mlhs_paren(targets) end # () # ^^ # # (1) # ^^^ def visit_parentheses_node(node) body = if node.body.nil? on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.body) end bounds(node.location) on_paren(body) end # foo => ^(bar) # ^^^^^^ def visit_pinned_expression_node(node) expression = visit(node.expression) bounds(node.location) on_begin(expression) end # foo = 1 and bar => ^foo # ^^^^ def visit_pinned_variable_node(node) visit(node.variable) end # END {} # ^^^^^^ def visit_post_execution_node(node) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.location) on_END(statements) end # BEGIN {} # ^^^^^^^^ def visit_pre_execution_node(node) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.location) on_BEGIN(statements) end # The top-level program node. def visit_program_node(node) body = node.statements.body body << nil if body.empty? statements = visit_statements_node_body(body) bounds(node.location) on_program(statements) end # 0..5 # ^^^^ def visit_range_node(node) left = visit(node.left) right = visit(node.right) bounds(node.location) if node.exclude_end? on_dot3(left, right) else on_dot2(left, right) end end # 1r # ^^ def visit_rational_node(node) visit_number_node(node) { |text| on_rational(text) } end # redo # ^^^^ def visit_redo_node(node) bounds(node.location) on_redo end # /foo/ # ^^^^^ def visit_regular_expression_node(node) bounds(node.opening_loc) on_regexp_beg(node.opening) if node.content.empty? bounds(node.closing_loc) closing = on_regexp_end(node.closing) on_regexp_literal(on_regexp_new, closing) else bounds(node.content_loc) tstring_content = on_tstring_content(node.content) bounds(node.closing_loc) closing = on_regexp_end(node.closing) on_regexp_literal(on_regexp_add(on_regexp_new, tstring_content), closing) end end # def foo(bar:); end # ^^^^ def visit_required_keyword_parameter_node(node) bounds(node.name_loc) [on_label("#{node.name}:"), false] end # def foo(bar); end # ^^^ def visit_required_parameter_node(node) bounds(node.location) on_ident(node.name.to_s) end # foo rescue bar # ^^^^^^^^^^^^^^ def visit_rescue_modifier_node(node) expression = visit_write_value(node.expression) rescue_expression = visit(node.rescue_expression) bounds(node.location) on_rescue_mod(expression, rescue_expression) end # begin; rescue; end # ^^^^^^^ def visit_rescue_node(node) exceptions = case node.exceptions.length when 0 nil when 1 if (exception = node.exceptions.first).is_a?(SplatNode) bounds(exception.location) on_mrhs_add_star(on_mrhs_new, visit(exception)) else [visit(node.exceptions.first)] end else bounds(node.location) length = node.exceptions.length node.exceptions.each_with_index.inject(on_args_new) do |mrhs, (exception, index)| arg = visit(exception) bounds(exception.location) mrhs = on_mrhs_new_from_args(mrhs) if index == length - 1 if exception.is_a?(SplatNode) if index == length - 1 on_mrhs_add_star(mrhs, arg) else on_args_add_star(mrhs, arg) end else if index == length - 1 on_mrhs_add(mrhs, arg) else on_args_add(mrhs, arg) end end end end reference = visit(node.reference) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end subsequent = visit(node.subsequent) bounds(node.location) on_rescue(exceptions, reference, statements, subsequent) end # def foo(*bar); end # ^^^^ # # def foo(*); end # ^ def visit_rest_parameter_node(node) if node.name_loc.nil? bounds(node.location) on_rest_param(nil) else bounds(node.name_loc) on_rest_param(visit_token(node.name.to_s)) end end # retry # ^^^^^ def visit_retry_node(node) bounds(node.location) on_retry end # return # ^^^^^^ # # return 1 # ^^^^^^^^ def visit_return_node(node) if node.arguments.nil? bounds(node.location) on_return0 else arguments = visit(node.arguments) bounds(node.location) on_return(arguments) end end # self # ^^^^ def visit_self_node(node) bounds(node.location) on_var_ref(on_kw("self")) end # A shareable constant. def visit_shareable_constant_node(node) visit(node.write) end # class << self; end # ^^^^^^^^^^^^^^^^^^ def visit_singleton_class_node(node) expression = visit(node.expression) bodystmt = visit_body_node(node.body&.location || node.end_keyword_loc, node.body) bounds(node.location) on_sclass(expression, bodystmt) end # __ENCODING__ # ^^^^^^^^^^^^ def visit_source_encoding_node(node) bounds(node.location) on_var_ref(on_kw("__ENCODING__")) end # __FILE__ # ^^^^^^^^ def visit_source_file_node(node) bounds(node.location) on_var_ref(on_kw("__FILE__")) end # __LINE__ # ^^^^^^^^ def visit_source_line_node(node) bounds(node.location) on_var_ref(on_kw("__LINE__")) end # foo(*bar) # ^^^^ # # def foo((bar, *baz)); end # ^^^^ # # def foo(*); bar(*); end # ^ def visit_splat_node(node) visit(node.expression) end # A list of statements. def visit_statements_node(node) bounds(node.location) visit_statements_node_body(node.body) end # Visit the list of statements of a statements node. We support nil # statements in the list. This would normally not be allowed by the # structure of the prism parse tree, but we manually add them here so that # we can mirror Ripper's void stmt. private def visit_statements_node_body(body) body.inject(on_stmts_new) do |stmts, stmt| on_stmts_add(stmts, stmt.nil? ? on_void_stmt : visit(stmt)) end end # "foo" # ^^^^^ def visit_string_node(node) if (content = node.content).empty? bounds(node.location) on_string_literal(on_string_content) elsif (opening = node.opening) == "?" bounds(node.location) on_CHAR("?#{node.content}") elsif opening.start_with?("<<~") heredoc = visit_heredoc_string_node(node.to_interpolated) bounds(node.location) on_string_literal(heredoc) else bounds(node.content_loc) tstring_content = on_tstring_content(content) bounds(node.location) on_string_literal(on_string_add(on_string_content, tstring_content)) end end # Ripper gives back the escaped string content but strips out the common # leading whitespace. Prism gives back the unescaped string content and # a location for the escaped string content. Unfortunately these don't # work well together, so here we need to re-derive the common leading # whitespace. private def visit_heredoc_node_whitespace(parts) common_whitespace = nil dedent_next = true parts.each do |part| if part.is_a?(StringNode) if dedent_next && !(content = part.content).chomp.empty? common_whitespace = [ common_whitespace || Float::INFINITY, content[/\A\s*/].each_char.inject(0) do |part_whitespace, char| char == "\t" ? ((part_whitespace / 8 + 1) * 8) : (part_whitespace + 1) end ].min end dedent_next = true else dedent_next = false end end common_whitespace || 0 end # Visit a string that is expressed using a <<~ heredoc. private def visit_heredoc_node(parts, base) common_whitespace = visit_heredoc_node_whitespace(parts) if common_whitespace == 0 bounds(parts.first.location) string = [] result = base parts.each do |part| if part.is_a?(StringNode) if string.empty? string = [part] else string << part end else unless string.empty? bounds(string[0].location) result = yield result, on_tstring_content(string.map(&:content).join) string = [] end result = yield result, visit(part) end end unless string.empty? bounds(string[0].location) result = yield result, on_tstring_content(string.map(&:content).join) end result else bounds(parts.first.location) result = parts.inject(base) do |string_content, part| yield string_content, visit_string_content(part) end bounds(parts.first.location) on_heredoc_dedent(result, common_whitespace) end end # Visit a heredoc node that is representing a string. private def visit_heredoc_string_node(node) bounds(node.opening_loc) on_heredoc_beg(node.opening) bounds(node.location) result = visit_heredoc_node(node.parts, on_string_content) do |parts, part| on_string_add(parts, part) end bounds(node.closing_loc) on_heredoc_end(node.closing) result end # Visit a heredoc node that is representing an xstring. private def visit_heredoc_x_string_node(node) bounds(node.opening_loc) on_heredoc_beg(node.opening) bounds(node.location) result = visit_heredoc_node(node.parts, on_xstring_new) do |parts, part| on_xstring_add(parts, part) end bounds(node.closing_loc) on_heredoc_end(node.closing) result end # super(foo) # ^^^^^^^^^^ def visit_super_node(node) arguments, block = visit_call_node_arguments(node.arguments, node.block, trailing_comma?(node.arguments&.location || node.location, node.rparen_loc || node.location)) if !node.lparen_loc.nil? bounds(node.lparen_loc) arguments = on_arg_paren(arguments) end bounds(node.location) call = on_super(arguments) if block.nil? call else bounds(node.block.location) on_method_add_block(call, block) end end # :foo # ^^^^ def visit_symbol_node(node) if (opening = node.opening)&.match?(/^%s|['"]:?$/) bounds(node.value_loc) content = on_string_content if !(value = node.value).empty? content = on_string_add(content, on_tstring_content(value)) end on_dyna_symbol(content) elsif (closing = node.closing) == ":" bounds(node.location) on_label("#{node.value}:") elsif opening.nil? && node.closing_loc.nil? bounds(node.value_loc) on_symbol_literal(visit_token(node.value)) else bounds(node.value_loc) on_symbol_literal(on_symbol(visit_token(node.value))) end end # true # ^^^^ def visit_true_node(node) bounds(node.location) on_var_ref(on_kw("true")) end # undef foo # ^^^^^^^^^ def visit_undef_node(node) names = visit_all(node.names) bounds(node.location) on_undef(names) end # unless foo; bar end # ^^^^^^^^^^^^^^^^^^^ # # bar unless foo # ^^^^^^^^^^^^^^ def visit_unless_node(node) if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset) predicate = visit(node.predicate) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end else_clause = visit(node.else_clause) bounds(node.location) on_unless(predicate, statements, else_clause) else statements = visit(node.statements.body.first) predicate = visit(node.predicate) bounds(node.location) on_unless_mod(predicate, statements) end end # until foo; bar end # ^^^^^^^^^^^^^^^^^ # # bar until foo # ^^^^^^^^^^^^^ def visit_until_node(node) if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset) predicate = visit(node.predicate) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.location) on_until(predicate, statements) else statements = visit(node.statements.body.first) predicate = visit(node.predicate) bounds(node.location) on_until_mod(predicate, statements) end end # case foo; when bar; end # ^^^^^^^^^^^^^ def visit_when_node(node) # This is a special case where we're not going to call on_when directly # because we don't have access to the subsequent. Instead, we'll return # the component parts and let the parent node handle it. conditions = visit_arguments(node.conditions) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end [conditions, statements] end # while foo; bar end # ^^^^^^^^^^^^^^^^^^ # # bar while foo # ^^^^^^^^^^^^^ def visit_while_node(node) if node.statements.nil? || (node.predicate.location.start_offset < node.statements.location.start_offset) predicate = visit(node.predicate) statements = if node.statements.nil? bounds(node.location) on_stmts_add(on_stmts_new, on_void_stmt) else visit(node.statements) end bounds(node.location) on_while(predicate, statements) else statements = visit(node.statements.body.first) predicate = visit(node.predicate) bounds(node.location) on_while_mod(predicate, statements) end end # `foo` # ^^^^^ def visit_x_string_node(node) if node.unescaped.empty? bounds(node.location) on_xstring_literal(on_xstring_new) elsif node.opening.start_with?("<<~") heredoc = visit_heredoc_x_string_node(node.to_interpolated) bounds(node.location) on_xstring_literal(heredoc) else bounds(node.content_loc) content = on_tstring_content(node.content) bounds(node.location) on_xstring_literal(on_xstring_add(on_xstring_new, content)) end end # yield # ^^^^^ # # yield 1 # ^^^^^^^ def visit_yield_node(node) if node.arguments.nil? && node.lparen_loc.nil? bounds(node.location) on_yield0 else arguments = if node.arguments.nil? bounds(node.location) on_args_new else visit(node.arguments) end unless node.lparen_loc.nil? bounds(node.lparen_loc) arguments = on_paren(arguments) end bounds(node.location) on_yield(arguments) end end private # Lazily initialize the parse result. def result @result ||= Prism.parse(source, partial_script: true, version: "current") end ########################################################################## # Helpers ########################################################################## # Returns true if there is a comma between the two locations. def trailing_comma?(left, right) source.byteslice(left.end_offset...right.start_offset).include?(",") end # Returns true if there is a semicolon between the two locations. def void_stmt?(left, right, allow_newline) pattern = allow_newline ? /[;\n]/ : /;/ source.byteslice(left.end_offset...right.start_offset).match?(pattern) end # Visit the string content of a particular node. This method is used to # split into the various token types. def visit_token(token, allow_keywords = true) case token when "." on_period(token) when "`" on_backtick(token) when *(allow_keywords ? KEYWORDS : []) on_kw(token) when /^_/ on_ident(token) when /^[[:upper:]]\w*$/ on_const(token) when /^@@/ on_cvar(token) when /^@/ on_ivar(token) when /^\$/ on_gvar(token) when /^[[:punct:]]/ on_op(token) else on_ident(token) end end # Visit a node that represents a number. We need to explicitly handle the # unary - operator. def visit_number_node(node) slice = node.slice location = node.location if slice[0] == "-" bounds(location.copy(start_offset: location.start_offset + 1)) value = yield slice[1..-1] bounds(node.location) on_unary(:-@, value) else bounds(location) yield slice end end # Visit a node that represents a write value. This is used to handle the # special case of an implicit array that is generated without brackets. def visit_write_value(node) if node.is_a?(ArrayNode) && node.opening_loc.nil? elements = node.elements length = elements.length bounds(elements.first.location) elements.each_with_index.inject((elements.first.is_a?(SplatNode) && length == 1) ? on_mrhs_new : on_args_new) do |args, (element, index)| arg = visit(element) bounds(element.location) if index == length - 1 if element.is_a?(SplatNode) mrhs = index == 0 ? args : on_mrhs_new_from_args(args) on_mrhs_add_star(mrhs, arg) else on_mrhs_add(on_mrhs_new_from_args(args), arg) end else case element when BlockArgumentNode on_args_add_block(args, arg) when SplatNode on_args_add_star(args, arg) else on_args_add(args, arg) end end end else visit(node) end end # This method is responsible for updating lineno and column information # to reflect the current node. # # This method could be drastically improved with some caching on the start # of every line, but for now it's good enough. def bounds(location) @lineno = location.start_line @column = location.start_column end ########################################################################## # Ripper interface ########################################################################## # :stopdoc: def _dispatch_0; end def _dispatch_1(_); end def _dispatch_2(_, _); end def _dispatch_3(_, _, _); end def _dispatch_4(_, _, _, _); end def _dispatch_5(_, _, _, _, _); end def _dispatch_7(_, _, _, _, _, _, _); end # :startdoc: # # Parser Events # PARSER_EVENT_TABLE.each do |id, arity| alias_method "on_#{id}", "_dispatch_#{arity}" end # This method is called when weak warning is produced by the parser. # +fmt+ and +args+ is printf style. def warn(fmt, *args) end # This method is called when strong warning is produced by the parser. # +fmt+ and +args+ is printf style. def warning(fmt, *args) end # This method is called when the parser found syntax error. def compile_error(msg) end # # Scanner Events # SCANNER_EVENTS.each do |id| alias_method "on_#{id}", :_dispatch_1 end # This method is provided by the Ripper C extension. It is called when a # string needs to be dedented because of a tilde heredoc. It is expected # that it will modify the string in place and return the number of bytes # that were removed. def dedent_string(string, width) whitespace = 0 cursor = 0 while cursor < string.length && string[cursor].match?(/\s/) && whitespace < width if string[cursor] == "\t" whitespace = ((whitespace / 8 + 1) * 8) break if whitespace > width else whitespace += 1 end cursor += 1 end string.replace(string[cursor..]) cursor end end end end PK!Gݩ88 parser.rbnu[# frozen_string_literal: true # :markup: markdown begin required_version = ">= 3.3.7.2" gem "parser", required_version require "parser" rescue LoadError warn(<<~MSG) Error: Unable to load parser #{required_version}. \ Add `gem "parser"` to your Gemfile or run `bundle update parser`. MSG exit(1) end module Prism module Translation # This class is the entry-point for converting a prism syntax tree into the # whitequark/parser gem's syntax tree. It inherits from the base parser for # the parser gem, and overrides the parse* methods to parse with prism and # then translate. # # Note that this version of the parser always parses using the latest # version of Ruby syntax supported by Prism. If you want specific version # support, use one of the version-specific subclasses, such as # `Prism::Translation::Parser34`. If you want to parse using the same # version of Ruby syntax as the currently running version of Ruby, use # `Prism::Translation::ParserCurrent`. class Parser < ::Parser::Base Diagnostic = ::Parser::Diagnostic # :nodoc: private_constant :Diagnostic # The parser gem has a list of diagnostics with a hard-coded set of error # messages. We create our own diagnostic class in order to set our own # error messages. class PrismDiagnostic < Diagnostic # This is the cached message coming from prism. attr_reader :message # Initialize a new diagnostic with the given message and location. def initialize(message, level, reason, location) @message = message super(level, reason, {}, location, []) end end Racc_debug_parser = false # :nodoc: # The `builder` argument is used to create the parser using our custom builder class by default. # # By using the `:parser` keyword argument, you can translate in a way that is compatible with # the Parser gem using any parser. # # For example, in RuboCop for Ruby LSP, the following approach can be used to improve performance # by reusing a pre-parsed `Prism::ParseLexResult`: # # class PrismPreparsed # def initialize(prism_result) # @prism_result = prism_result # end # # def parse_lex(source, **options) # @prism_result # end # end # # prism_preparsed = PrismPreparsed.new(prism_result) # # Prism::Translation::Ruby34.new(builder, parser: prism_preparsed) # # In an object passed to the `:parser` keyword argument, the `parse` and `parse_lex` methods # should be implemented as needed. # def initialize(builder = Prism::Translation::Parser::Builder.new, parser: Prism) if !builder.is_a?(Prism::Translation::Parser::Builder) warn(<<~MSG, uplevel: 1, category: :deprecated) [deprecation]: The builder passed to `Prism::Translation::Parser.new` is not a \ `Prism::Translation::Parser::Builder` subclass. This will raise in the next major version. MSG end @parser = parser super(builder) end def version # :nodoc: 41 end # The default encoding for Ruby files is UTF-8. def default_encoding Encoding::UTF_8 end def yyerror # :nodoc: end # Parses a source buffer and returns the AST. def parse(source_buffer) @source_buffer = source_buffer source = source_buffer.source offset_cache = build_offset_cache(source) result = unwrap(@parser.parse(source, **prism_options), offset_cache) build_ast(result.value, offset_cache) ensure @source_buffer = nil end # Parses a source buffer and returns the AST and the source code comments. def parse_with_comments(source_buffer) @source_buffer = source_buffer source = source_buffer.source offset_cache = build_offset_cache(source) result = unwrap(@parser.parse(source, **prism_options), offset_cache) [ build_ast(result.value, offset_cache), build_comments(result.comments, offset_cache) ] ensure @source_buffer = nil end # Parses a source buffer and returns the AST, the source code comments, # and the tokens emitted by the lexer. def tokenize(source_buffer, recover = false) @source_buffer = source_buffer source = source_buffer.source offset_cache = build_offset_cache(source) result = begin unwrap(@parser.parse_lex(source, **prism_options), offset_cache) rescue ::Parser::SyntaxError raise if !recover end program, tokens = result.value ast = build_ast(program, offset_cache) if result.success? [ ast, build_comments(result.comments, offset_cache), build_tokens(tokens, offset_cache) ] ensure @source_buffer = nil end # Since prism resolves num params for us, we don't need to support this # kind of logic here. def try_declare_numparam(node) node.children[0].match?(/\A_[1-9]\z/) end private # This is a hook to allow consumers to disable some errors if they don't # want them to block creating the syntax tree. def valid_error?(error) true end # This is a hook to allow consumers to disable some warnings if they don't # want them to block creating the syntax tree. def valid_warning?(warning) true end # Build a diagnostic from the given prism parse error. def error_diagnostic(error, offset_cache) location = error.location diagnostic_location = build_range(location, offset_cache) case error.type when :argument_block_multi Diagnostic.new(:error, :block_and_blockarg, {}, diagnostic_location, []) when :argument_formal_constant Diagnostic.new(:error, :argument_const, {}, diagnostic_location, []) when :argument_formal_class Diagnostic.new(:error, :argument_cvar, {}, diagnostic_location, []) when :argument_formal_global Diagnostic.new(:error, :argument_gvar, {}, diagnostic_location, []) when :argument_formal_ivar Diagnostic.new(:error, :argument_ivar, {}, diagnostic_location, []) when :argument_no_forwarding_amp Diagnostic.new(:error, :no_anonymous_blockarg, {}, diagnostic_location, []) when :argument_no_forwarding_star Diagnostic.new(:error, :no_anonymous_restarg, {}, diagnostic_location, []) when :argument_no_forwarding_star_star Diagnostic.new(:error, :no_anonymous_kwrestarg, {}, diagnostic_location, []) when :begin_lonely_else location = location.copy(length: 4) diagnostic_location = build_range(location, offset_cache) Diagnostic.new(:error, :useless_else, {}, diagnostic_location, []) when :class_name, :module_name Diagnostic.new(:error, :module_name_const, {}, diagnostic_location, []) when :class_in_method Diagnostic.new(:error, :class_in_def, {}, diagnostic_location, []) when :def_endless_setter Diagnostic.new(:error, :endless_setter, {}, diagnostic_location, []) when :embdoc_term Diagnostic.new(:error, :embedded_document, {}, diagnostic_location, []) when :incomplete_variable_class, :incomplete_variable_class_3_3 location = location.copy(length: location.length + 1) diagnostic_location = build_range(location, offset_cache) Diagnostic.new(:error, :cvar_name, { name: location.slice }, diagnostic_location, []) when :incomplete_variable_instance, :incomplete_variable_instance_3_3 location = location.copy(length: location.length + 1) diagnostic_location = build_range(location, offset_cache) Diagnostic.new(:error, :ivar_name, { name: location.slice }, diagnostic_location, []) when :invalid_variable_global, :invalid_variable_global_3_3 Diagnostic.new(:error, :gvar_name, { name: location.slice }, diagnostic_location, []) when :module_in_method Diagnostic.new(:error, :module_in_def, {}, diagnostic_location, []) when :numbered_parameter_ordinary Diagnostic.new(:error, :ordinary_param_defined, {}, diagnostic_location, []) when :numbered_parameter_outer_scope Diagnostic.new(:error, :numparam_used_in_outer_scope, {}, diagnostic_location, []) when :parameter_circular Diagnostic.new(:error, :circular_argument_reference, { var_name: location.slice }, diagnostic_location, []) when :parameter_name_repeat Diagnostic.new(:error, :duplicate_argument, {}, diagnostic_location, []) when :parameter_numbered_reserved Diagnostic.new(:error, :reserved_for_numparam, { name: location.slice }, diagnostic_location, []) when :regexp_unknown_options Diagnostic.new(:error, :regexp_options, { options: location.slice[1..] }, diagnostic_location, []) when :singleton_for_literals Diagnostic.new(:error, :singleton_literal, {}, diagnostic_location, []) when :string_literal_eof Diagnostic.new(:error, :string_eof, {}, diagnostic_location, []) when :unexpected_token_ignore Diagnostic.new(:error, :unexpected_token, { token: location.slice }, diagnostic_location, []) when :write_target_in_method Diagnostic.new(:error, :dynamic_const, {}, diagnostic_location, []) else PrismDiagnostic.new(error.message, :error, error.type, diagnostic_location) end end # Build a diagnostic from the given prism parse warning. def warning_diagnostic(warning, offset_cache) diagnostic_location = build_range(warning.location, offset_cache) case warning.type when :ambiguous_first_argument_plus Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "+" }, diagnostic_location, []) when :ambiguous_first_argument_minus Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "-" }, diagnostic_location, []) when :ambiguous_prefix_ampersand Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "&" }, diagnostic_location, []) when :ambiguous_prefix_star Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "*" }, diagnostic_location, []) when :ambiguous_prefix_star_star Diagnostic.new(:warning, :ambiguous_prefix, { prefix: "**" }, diagnostic_location, []) when :ambiguous_slash Diagnostic.new(:warning, :ambiguous_regexp, {}, diagnostic_location, []) when :dot_dot_dot_eol Diagnostic.new(:warning, :triple_dot_at_eol, {}, diagnostic_location, []) when :duplicated_hash_key # skip, parser does this on its own else PrismDiagnostic.new(warning.message, :warning, warning.type, diagnostic_location) end end # If there was a error generated during the parse, then raise an # appropriate syntax error. Otherwise return the result. def unwrap(result, offset_cache) result.errors.each do |error| next unless valid_error?(error) diagnostics.process(error_diagnostic(error, offset_cache)) end result.warnings.each do |warning| next unless valid_warning?(warning) diagnostic = warning_diagnostic(warning, offset_cache) diagnostics.process(diagnostic) if diagnostic end result end # Prism deals with offsets in bytes, while the parser gem deals with # offsets in characters. We need to handle this conversion in order to # build the parser gem AST. # # If the bytesize of the source is the same as the length, then we can # just use the offset directly. Otherwise, we build an array where the # index is the byte offset and the value is the character offset. def build_offset_cache(source) if source.bytesize == source.length -> (offset) { offset } else offset_cache = [] offset = 0 source.each_char do |char| char.bytesize.times { offset_cache << offset } offset += 1 end offset_cache << offset end end # Build the parser gem AST from the prism AST. def build_ast(program, offset_cache) program.accept(Compiler.new(self, offset_cache)) end # Build the parser gem comments from the prism comments. def build_comments(comments, offset_cache) comments.map do |comment| ::Parser::Source::Comment.new(build_range(comment.location, offset_cache)) end end # Build the parser gem tokens from the prism tokens. def build_tokens(tokens, offset_cache) Lexer.new(source_buffer, tokens, offset_cache).to_a end # Build a range from a prism location. def build_range(location, offset_cache) ::Parser::Source::Range.new( source_buffer, offset_cache[location.start_offset], offset_cache[location.end_offset] ) end # Options for how prism should parse/lex the source. def prism_options options = { filepath: @source_buffer.name, version: convert_for_prism(version), partial_script: true, } # The parser gem always encodes to UTF-8, unless it is binary. # https://github.com/whitequark/parser/blob/v3.3.6.0/lib/parser/source/buffer.rb#L80-L107 options[:encoding] = false if @source_buffer.source.encoding != Encoding::BINARY options end # Converts the version format handled by Parser to the format handled by Prism. def convert_for_prism(version) case version when 33 "3.3.1" when 34 "3.4.0" when 35, 40 "4.0.0" when 41 "4.1.0" else "latest" end end require_relative "parser/builder" require_relative "parser/compiler" require_relative "parser/lexer" private_constant :Compiler private_constant :Lexer end end end PK!,8%Writer/TranslationWriterInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib */ interface TranslationWriterInterface { /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []); } PK!8"NNWriter/TranslationWriter.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Dumper\DumperInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib */ class TranslationWriter implements TranslationWriterInterface { private $dumpers = []; /** * Adds a dumper to the writer. * * @param string $format The format of the dumper */ public function addDumper($format, DumperInterface $dumper) { $this->dumpers[$format] = $dumper; } /** * Obtains the list of supported formats. * * @return array */ public function getFormats() { return array_keys($this->dumpers); } /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []) { if (!isset($this->dumpers[$format])) { throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format)); } // get the right dumper $dumper = $this->dumpers[$format]; if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) { throw new RuntimeException(sprintf('Translation Writer was not able to create directory "%s".', $options['path'])); } // save $dumper->dump($catalogue, $options); } } PK!c|:%Reader/TranslationReaderInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Tobias Nyholm */ interface TranslationReaderInterface { /** * Reads translation messages from a directory to the catalogue. */ public function read(string $directory, MessageCatalogue $catalogue); } PK!ȳ}Reader/TranslationReader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Michel Salib */ class TranslationReader implements TranslationReaderInterface { /** * Loaders used for import. * * @var array */ private $loaders = []; /** * Adds a loader to the translation extractor. * * @param string $format The format of the loader */ public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } /** * {@inheritdoc} */ public function read(string $directory, MessageCatalogue $catalogue) { if (!is_dir($directory)) { return; } foreach ($this->loaders as $format => $loader) { // load any existing translation files $finder = new Finder(); $extension = $catalogue->getLocale().'.'.$format; $files = $finder->files()->name('*.'.$extension)->in($directory); foreach ($files as $file) { $domain = substr($file->getFilename(), 0, -1 * \strlen($extension) - 1); $catalogue->addCatalogue($loader->load($file->getPathname(), $catalogue->getLocale(), $domain)); } } } } PK!< b`` CHANGELOG.mdnuIwCHANGELOG ========= 5.2.0 ----- * added support for calling `trans` with ICU formatted messages * added `PseudoLocalizationTranslator` * added `TranslatableMessage` objects that represent a message that can be translated * added the `t()` function to easily create `TranslatableMessage` objects * Added support for extracting messages from `TranslatableMessage` objects 5.1.0 ----- * added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element 5.0.0 ----- * removed support for using `null` as the locale in `Translator` * removed `TranslatorInterface` * removed `MessageSelector` * removed `ChoiceMessageFormatterInterface` * removed `PluralizationRule` * removed `Interval` * removed `transChoice()` methods, use the trans() method instead with a %count% parameter * removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()` * removed `MessageFormatter::choiceFormat()` * added argument `$filename` to `PhpExtractor::parseTokens()` * removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. 4.4.0 ----- * deprecated support for using `null` as the locale in `Translator` * deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. * Marked the `TranslationDataCollector` class as `@final`. 4.3.0 ----- * Improved Xliff 1.2 loader to load the original file's metadata * Added `TranslatorPathsPass` 4.2.0 ----- * Started using ICU parent locales as fallback locales. * allow using the ICU message format using domains with the "+intl-icu" suffix * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead * Added `IntlFormatter` and `IntlFormatterInterface` * added support for multiple files and directories in `XliffLintCommand` * Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal 4.1.0 ----- * The `FileDumper::setBackup()` method is deprecated. * The `TranslationWriter::disableBackup()` method is deprecated. * The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0. 4.0.0 ----- * removed the backup feature of the `FileDumper` class * removed `TranslationWriter::writeTranslations()` method * removed support for passing `MessageSelector` instances to the constructor of the `Translator` class 3.4.0 ----- * Added `TranslationDumperPass` * Added `TranslationExtractorPass` * Added `TranslatorPass` * Added `TranslationReader` and `TranslationReaderInterface` * Added `` section to the Xliff 2.0 dumper. * Improved Xliff 2.0 loader to load `` section. * Added `TranslationWriterInterface` * Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write` * added support for adding custom message formatter and decoupling the default one. * Added `PhpExtractor` * Added `PhpStringTokenParser` 3.2.0 ----- * Added support for escaping `|` in plural translations with double pipe. 3.1.0 ----- * Deprecated the backup feature of the file dumper classes. 3.0.0 ----- * removed `FileDumper::format()` method. * Changed the visibility of the locale property in `Translator` from protected to private. 2.8.0 ----- * deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead. * deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead. * added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file. * added option `json_encoding` to JsonFileDumper * added options `as_tree`, `inline` to YamlFileDumper * added support for XLIFF 2.0. * added support for XLIFF target and tool attributes. * added message parameters to DataCollectorTranslator. * [DEPRECATION] The `DiffOperation` class has been deprecated and will be removed in Symfony 3.0, since its operation has nothing to do with 'diff', so the class name is misleading. The `TargetOperation` class should be used for this use-case instead. 2.7.0 ----- * added DataCollectorTranslator for collecting the translated messages. 2.6.0 ----- * added possibility to cache catalogues * added TranslatorBagInterface * added LoggingTranslator * added Translator::getMessages() for retrieving the message catalogue as an array 2.5.0 ----- * added relative file path template to the file dumpers * added optional backup to the file dumpers * changed IcuResFileDumper to extend FileDumper 2.3.0 ----- * added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues) * added Translator::getFallbackLocales() * deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method 2.2.0 ----- * QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3. * [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid. * changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException (IcuDatFileLoader, IcuResFileLoader and QtFileLoader) 2.1.0 ----- * added support for more than one fallback locale * added support for extracting translation messages from templates (Twig and PHP) * added dumpers for translation catalogs * added support for QT, gettext, and ResourceBundles PK!_ README.mdnuIwTranslation Component ===================== The Translation component provides tools to internationalize your application. Getting Started --------------- ``` $ composer require symfony/translation ``` ```php use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\ArrayLoader; $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'Hello World!' => 'Bonjour !', ], 'fr_FR'); echo $translator->trans('Hello World!'); // outputs « Bonjour ! » ``` Resources --------- * [Documentation](https://symfony.com/doc/current/translation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) PK!ꪴ\\"Extractor/PhpStringTokenParser.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; /* * The following is derived from code at http://github.com/nikic/PHP-Parser * * Copyright (c) 2011 by Nikita Popov * * Some rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * * The names of the contributors may not be used to endorse or * promote products derived from this software without specific * prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class PhpStringTokenParser { protected static $replacements = [ '\\' => '\\', '$' => '$', 'n' => "\n", 'r' => "\r", 't' => "\t", 'f' => "\f", 'v' => "\v", 'e' => "\x1B", ]; /** * Parses a string token. * * @param string $str String token content * * @return string The parsed string */ public static function parse(string $str) { $bLength = 0; if ('b' === $str[0]) { $bLength = 1; } if ('\'' === $str[$bLength]) { return str_replace( ['\\\\', '\\\''], ['\\', '\''], substr($str, $bLength + 1, -1) ); } else { return self::parseEscapeSequences(substr($str, $bLength + 1, -1), '"'); } } /** * Parses escape sequences in strings (all string types apart from single quoted). * * @param string $str String without quotes * @param string|null $quote Quote type * * @return string String with escape sequences parsed */ public static function parseEscapeSequences(string $str, string $quote = null) { if (null !== $quote) { $str = str_replace('\\'.$quote, $quote, $str); } return preg_replace_callback( '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3})~', [__CLASS__, 'parseCallback'], $str ); } private static function parseCallback(array $matches): string { $str = $matches[1]; if (isset(self::$replacements[$str])) { return self::$replacements[$str]; } elseif ('x' === $str[0] || 'X' === $str[0]) { return \chr(hexdec($str)); } else { return \chr(octdec($str)); } } /** * Parses a constant doc string. * * @param string $startToken Doc string start token content (<< * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * Base class used by classes that extract translation messages from files. * * @author Marcos D. Sánchez */ abstract class AbstractFileExtractor { /** * @param string|iterable $resource Files, a file or a directory * * @return iterable */ protected function extractFiles($resource) { if (is_iterable($resource)) { $files = []; foreach ($resource as $file) { if ($this->canBeExtracted($file)) { $files[] = $this->toSplFileInfo($file); } } } elseif (is_file($resource)) { $files = $this->canBeExtracted($resource) ? [$this->toSplFileInfo($resource)] : []; } else { $files = $this->extractFromDirectory($resource); } return $files; } private function toSplFileInfo(string $file): \SplFileInfo { return new \SplFileInfo($file); } /** * @return bool * * @throws InvalidArgumentException */ protected function isFile(string $file) { if (!is_file($file)) { throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file)); } return true; } /** * @return bool */ abstract protected function canBeExtracted(string $file); /** * @param string|array $resource Files, a file or a directory * * @return iterable files to be extracted */ abstract protected function extractFromDirectory($resource); } PK!##Extractor/PhpExtractor.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\MessageCatalogue; /** * PhpExtractor extracts translation messages from a PHP template. * * @author Michel Salib */ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface { public const MESSAGE_TOKEN = 300; public const METHOD_ARGUMENTS_TOKEN = 1000; public const DOMAIN_TOKEN = 1001; /** * Prefix for new found message. * * @var string */ private $prefix = ''; /** * The sequence that captures translation messages. * * @var array */ protected $sequences = [ [ '->', 'trans', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ '->', 'trans', '(', self::MESSAGE_TOKEN, ], [ 'new', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 'new', '\\', 'Symfony', '\\', 'Component', '\\', 'Translation', '\\', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', '\Symfony\Component\Translation\TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 'new', '\\', 'Symfony', '\\', 'Component', '\\', 'Translation', '\\', 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 'new', '\Symfony\Component\Translation\TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ 't', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ 't', '(', self::MESSAGE_TOKEN, ], ]; /** * {@inheritdoc} */ public function extract($resource, MessageCatalogue $catalog) { $files = $this->extractFiles($resource); foreach ($files as $file) { $this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file); gc_mem_caches(); } } /** * {@inheritdoc} */ public function setPrefix(string $prefix) { $this->prefix = $prefix; } /** * Normalizes a token. * * @param mixed $token * * @return string|null */ protected function normalizeToken($token) { if (isset($token[1]) && 'b"' !== $token) { return $token[1]; } return $token; } /** * Seeks to a non-whitespace token. */ private function seekToNextRelevantToken(\Iterator $tokenIterator) { for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if (\T_WHITESPACE !== $t[0]) { break; } } } private function skipMethodArgument(\Iterator $tokenIterator) { $openBraces = 0; for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if ('[' === $t[0] || '(' === $t[0]) { ++$openBraces; } if (']' === $t[0] || ')' === $t[0]) { --$openBraces; } if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) { break; } } } /** * Extracts the message from the iterator while the tokens * match allowed message tokens. */ private function getValue(\Iterator $tokenIterator) { $message = ''; $docToken = ''; $docPart = ''; for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); if ('.' === $t) { // Concatenate with next token continue; } if (!isset($t[1])) { break; } switch ($t[0]) { case \T_START_HEREDOC: $docToken = $t[1]; break; case \T_ENCAPSED_AND_WHITESPACE: case \T_CONSTANT_ENCAPSED_STRING: if ('' === $docToken) { $message .= PhpStringTokenParser::parse($t[1]); } else { $docPart = $t[1]; } break; case \T_END_HEREDOC: if ($indentation = strspn($t[1], ' ')) { $docPartWithLineBreaks = $docPart; $docPart = ''; foreach (preg_split('~(\r\n|\n|\r)~', $docPartWithLineBreaks, -1, \PREG_SPLIT_DELIM_CAPTURE) as $str) { if (\in_array($str, ["\r\n", "\n", "\r"], true)) { $docPart .= $str; } else { $docPart .= substr($str, $indentation); } } } $message .= PhpStringTokenParser::parseDocString($docToken, $docPart); $docToken = ''; $docPart = ''; break; case \T_WHITESPACE: break; default: break 2; } } return $message; } /** * Extracts trans message from PHP tokens. */ protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) { $tokenIterator = new \ArrayIterator($tokens); for ($key = 0; $key < $tokenIterator->count(); ++$key) { foreach ($this->sequences as $sequence) { $message = ''; $domain = 'messages'; $tokenIterator->seek($key); foreach ($sequence as $sequenceKey => $item) { $this->seekToNextRelevantToken($tokenIterator); if ($this->normalizeToken($tokenIterator->current()) === $item) { $tokenIterator->next(); continue; } elseif (self::MESSAGE_TOKEN === $item) { $message = $this->getValue($tokenIterator); if (\count($sequence) === ($sequenceKey + 1)) { break; } } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) { $this->skipMethodArgument($tokenIterator); } elseif (self::DOMAIN_TOKEN === $item) { $domainToken = $this->getValue($tokenIterator); if ('' !== $domainToken) { $domain = $domainToken; } break; } else { break; } } if ($message) { $catalog->set($message, $this->prefix.$message, $domain); $metadata = $catalog->getMetadata($message, $domain) ?? []; $normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename); $metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2]; $catalog->setMetadata($message, $metadata, $domain); break; } } } } /** * @return bool * * @throws \InvalidArgumentException */ protected function canBeExtracted(string $file) { return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION); } /** * {@inheritdoc} */ protected function extractFromDirectory($directory) { $finder = new Finder(); return $finder->files()->name('*.php')->in($directory); } } PK!/* Extractor/ExtractorInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * Extracts translation messages from a directory or files to the catalogue. * New found messages are injected to the catalogue using the prefix. * * @author Michel Salib */ interface ExtractorInterface { /** * Extracts translation messages from files, a file or a directory to the catalogue. * * @param string|array $resource Files, a file or a directory */ public function extract($resource, MessageCatalogue $catalogue); /** * Sets the prefix that should be used for new found messages. * * @param string $prefix The prefix */ public function setPrefix(string $prefix); } PK!xp:CCExtractor/ChainExtractor.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * ChainExtractor extracts translation messages from template files. * * @author Michel Salib */ class ChainExtractor implements ExtractorInterface { /** * The extractors. * * @var ExtractorInterface[] */ private $extractors = []; /** * Adds a loader to the translation extractor. * * @param string $format The format of the loader */ public function addExtractor(string $format, ExtractorInterface $extractor) { $this->extractors[$format] = $extractor; } /** * {@inheritdoc} */ public function setPrefix(string $prefix) { foreach ($this->extractors as $extractor) { $extractor->setPrefix($prefix); } } /** * {@inheritdoc} */ public function extract($directory, MessageCatalogue $catalogue) { foreach ($this->extractors as $extractor) { $extractor->extract($directory, $catalogue); } } } PK! ))LICENSEnuIwCopyright (c) 2004-2021 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!M  Loader/QtFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * QtFileLoader loads translations from QT Translations XML files. * * @author Benjamin Eberlei */ class QtFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { if (!class_exists(XmlUtils::class)) { throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.'); } if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $dom = XmlUtils::loadFile($resource); } catch (\InvalidArgumentException $e) { throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e); } $internalErrors = libxml_use_internal_errors(true); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]'); $catalogue = new MessageCatalogue($locale); if (1 == $nodes->length) { $translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message'); foreach ($translations as $translation) { $translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue; if (!empty($translationValue)) { $catalogue->set( (string) $translation->getElementsByTagName('source')->item(0)->nodeValue, $translationValue, $domain ); } $translation = $translation->nextSibling; } if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } } libxml_use_internal_errors($internalErrors); return $catalogue; } } PK!.!Loader/PhpFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * PhpFileLoader loads translations from PHP files returning an array of translations. * * @author Fabien Potencier */ class PhpFileLoader extends FileLoader { private static $cache = []; /** * {@inheritdoc} */ protected function loadResource($resource) { if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { self::$cache = null; } if (null === self::$cache) { return require $resource; } if (isset(self::$cache[$resource])) { return self::$cache[$resource]; } return self::$cache[$resource] = require $resource; } } PK!00Loader/IniFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * IniFileLoader loads translations from an ini file. * * @author stealth35 */ class IniFileLoader extends FileLoader { /** * {@inheritdoc} */ protected function loadResource($resource) { return parse_ini_file($resource, true); } } PK!ddLoader/LoaderInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * LoaderInterface is the interface implemented by all translation loaders. * * @author Fabien Potencier */ interface LoaderInterface { /** * Loads a locale. * * @param mixed $resource A resource * @param string $locale A locale * @param string $domain The domain * * @return MessageCatalogue A MessageCatalogue instance * * @throws NotFoundResourceException when the resource cannot be found * @throws InvalidResourceException when the resource cannot be loaded */ public function load($resource, string $locale, string $domain = 'messages'); } PK!=' ' Loader/IcuResFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuResFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!is_dir($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception $e) { $rb = null; } if (!$rb) { throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(DirectoryResource::class)) { $catalogue->addResource(new DirectoryResource($resource)); } return $catalogue; } /** * Flattens an ResourceBundle. * * The scheme used is: * key { key2 { key3 { "value" } } } * Becomes: * 'key.key2.key3' => 'value' * * This function takes an array by reference and will modify it * * @param \ResourceBundle $rb The ResourceBundle that will be flattened * @param array $messages Used internally for recursive calls * @param string $path Current path being parsed, used internally for recursive calls * * @return array the flattened ResourceBundle */ protected function flatten(\ResourceBundle $rb, array &$messages = [], string $path = null) { foreach ($rb as $key => $value) { $nodePath = $path ? $path.'.'.$key : $key; if ($value instanceof \ResourceBundle) { $this->flatten($value, $messages, $nodePath); } else { $messages[$nodePath] = $value; } } return $messages; } } PK!zLoader/FileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; /** * @author Abdellatif Ait boudad */ abstract class FileLoader extends ArrayLoader { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } $messages = $this->loadResource($resource); // empty resource if (null === $messages) { $messages = []; } // not an array if (!\is_array($messages)) { throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); } $catalogue = parent::load($messages, $locale, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } /** * @param string $resource * * @return array * * @throws InvalidResourceException if stream content has an invalid format */ abstract protected function loadResource($resource); } PK!jSLoader/ArrayLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\MessageCatalogue; /** * ArrayLoader loads translations from a PHP array. * * @author Fabien Potencier */ class ArrayLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { $resource = $this->flatten($resource); $catalogue = new MessageCatalogue($locale); $catalogue->add($resource, $domain); return $catalogue; } /** * Flattens an nested array of translations. * * The scheme used is: * 'key' => ['key2' => ['key3' => 'value']] * Becomes: * 'key.key2.key3' => 'value' */ private function flatten(array $messages): array { $result = []; foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { $result[$key.'.'.$k] = $v; } } else { $result[$key] = $value; } } return $result; } } PK!u'Loader/PoFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * @copyright Copyright (c) 2010, Union of RAD https://github.com/UnionOfRAD/lithium * @copyright Copyright (c) 2012, Clemens Tolboom */ class PoFileLoader extends FileLoader { /** * Parses portable object (PO) format. * * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files * we should be able to parse files having: * * white-space * # translator-comments * #. extracted-comments * #: reference... * #, flag... * #| msgid previous-untranslated-string * msgid untranslated-string * msgstr translated-string * * extra or different lines are: * * #| msgctxt previous-context * #| msgid previous-untranslated-string * msgctxt context * * #| msgid previous-untranslated-string-singular * #| msgid_plural previous-untranslated-string-plural * msgid untranslated-string-singular * msgid_plural untranslated-string-plural * msgstr[0] translated-string-case-0 * ... * msgstr[N] translated-string-case-n * * The definition states: * - white-space and comments are optional. * - msgid "" that an empty singleline defines a header. * * This parser sacrifices some features of the reference implementation the * differences to that implementation are as follows. * - No support for comments spanning multiple lines. * - Translator and extracted comments are treated as being the same type. * - Message IDs are allowed to have other encodings as just US-ASCII. * * Items with an empty id are ignored. * * {@inheritdoc} */ protected function loadResource($resource) { $stream = fopen($resource, 'r'); $defaults = [ 'ids' => [], 'translated' => null, ]; $messages = []; $item = $defaults; $flags = []; while ($line = fgets($stream)) { $line = trim($line); if ('' === $line) { // Whitespace indicated current item is done if (!\in_array('fuzzy', $flags)) { $this->addMessage($messages, $item); } $item = $defaults; $flags = []; } elseif ('#,' === substr($line, 0, 2)) { $flags = array_map('trim', explode(',', substr($line, 2))); } elseif ('msgid "' === substr($line, 0, 7)) { // We start a new msg so save previous // TODO: this fails when comments or contexts are added $this->addMessage($messages, $item); $item = $defaults; $item['ids']['singular'] = substr($line, 7, -1); } elseif ('msgstr "' === substr($line, 0, 8)) { $item['translated'] = substr($line, 8, -1); } elseif ('"' === $line[0]) { $continues = isset($item['translated']) ? 'translated' : 'ids'; if (\is_array($item[$continues])) { end($item[$continues]); $item[$continues][key($item[$continues])] .= substr($line, 1, -1); } else { $item[$continues] .= substr($line, 1, -1); } } elseif ('msgid_plural "' === substr($line, 0, 14)) { $item['ids']['plural'] = substr($line, 14, -1); } elseif ('msgstr[' === substr($line, 0, 7)) { $size = strpos($line, ']'); $item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1); } } // save last item if (!\in_array('fuzzy', $flags)) { $this->addMessage($messages, $item); } fclose($stream); return $messages; } /** * Save a translation item to the messages. * * A .po file could contain by error missing plural indexes. We need to * fix these before saving them. */ private function addMessage(array &$messages, array $item) { if (!empty($item['ids']['singular'])) { $id = stripcslashes($item['ids']['singular']); if (isset($item['ids']['plural'])) { $id .= '|'.stripcslashes($item['ids']['plural']); } $translated = (array) $item['translated']; // PO are by definition indexed so sort by index. ksort($translated); // Make sure every index is filled. end($translated); $count = key($translated); // Fill missing spots with '-'. $empties = array_fill(0, $count + 1, '-'); $translated += $empties; ksort($translated); $messages[$id] = stripcslashes(implode('|', $translated)); } } } PK!'Loader/JsonFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * JsonFileLoader loads translations from an json file. * * @author singles */ class JsonFileLoader extends FileLoader { /** * {@inheritdoc} */ protected function loadResource($resource) { $messages = []; if ($data = file_get_contents($resource)) { $messages = json_decode($data, true); if (0 < $errorCode = json_last_error()) { throw new InvalidResourceException('Error parsing JSON: '.$this->getJSONErrorMessage($errorCode)); } } return $messages; } /** * Translates JSON_ERROR_* constant into meaningful message. */ private function getJSONErrorMessage(int $errorCode): string { switch ($errorCode) { case \JSON_ERROR_DEPTH: return 'Maximum stack depth exceeded'; case \JSON_ERROR_STATE_MISMATCH: return 'Underflow or the modes mismatch'; case \JSON_ERROR_CTRL_CHAR: return 'Unexpected control character found'; case \JSON_ERROR_SYNTAX: return 'Syntax error, malformed JSON'; case \JSON_ERROR_UTF8: return 'Malformed UTF-8 characters, possibly incorrectly encoded'; default: return 'Unknown error'; } } } PK!|ccLoader/YamlFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Yaml; /** * YamlFileLoader loads translations from Yaml files. * * @author Fabien Potencier */ class YamlFileLoader extends FileLoader { private $yamlParser; /** * {@inheritdoc} */ protected function loadResource($resource) { if (null === $this->yamlParser) { if (!class_exists(\Symfony\Component\Yaml\Parser::class)) { throw new LogicException('Loading translations from the YAML format requires the Symfony Yaml component.'); } $this->yamlParser = new YamlParser(); } try { $messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e); } if (null !== $messages && !\is_array($messages)) { throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); } return $messages ?: []; } } PK!DoELoader/CsvFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\NotFoundResourceException; /** * CsvFileLoader loads translations from CSV files. * * @author Saša Stamenković */ class CsvFileLoader extends FileLoader { private $delimiter = ';'; private $enclosure = '"'; private $escape = '\\'; /** * {@inheritdoc} */ protected function loadResource($resource) { $messages = []; try { $file = new \SplFileObject($resource, 'rb'); } catch (\RuntimeException $e) { throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e); } $file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY); $file->setCsvControl($this->delimiter, $this->enclosure, $this->escape); foreach ($file as $data) { if (false === $data) { continue; } if ('#' !== substr($data[0], 0, 1) && isset($data[1]) && 2 === \count($data)) { $messages[$data[0]] = $data[1]; } } return $messages; } /** * Sets the delimiter, enclosure, and escape character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; $this->escape = $escape; } } PK!L8Loader/XliffFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\XliffUtils; /** * XliffFileLoader loads translations from XLIFF files. * * @author Fabien Potencier */ class XliffFileLoader implements LoaderInterface { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { if (!class_exists(XmlUtils::class)) { throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); } if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource)) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } $catalogue = new MessageCatalogue($locale); $this->extract($resource, $catalogue, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } private function extract($resource, MessageCatalogue $catalogue, string $domain) { try { $dom = XmlUtils::loadFile($resource); } catch (\InvalidArgumentException $e) { throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); } $xliffVersion = XliffUtils::getVersionNumber($dom); if ($errors = XliffUtils::validateSchema($dom)) { throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); } if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); } if ('2.0' === $xliffVersion) { $this->extractXliff2($dom, $catalogue, $domain); } } /** * Extract messages and metadata from DOMDocument into a MessageCatalogue. */ private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { $xml = simplexml_import_dom($dom); $encoding = strtoupper($dom->encoding); $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; $xml->registerXPathNamespace('xliff', $namespace); foreach ($xml->xpath('//xliff:file') as $file) { $fileAttributes = $file->attributes(); $file->registerXPathNamespace('xliff', $namespace); foreach ($file->xpath('.//xliff:trans-unit') as $translation) { $attributes = $translation->attributes(); if (!(isset($attributes['resname']) || isset($translation->source))) { continue; } $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding); $catalogue->set((string) $source, $target, $domain); $metadata = [ 'source' => (string) $translation->source, 'file' => [ 'original' => (string) $fileAttributes['original'], ], ]; if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { $metadata['notes'] = $notes; } if (isset($translation->target) && $translation->target->attributes()) { $metadata['target-attributes'] = []; foreach ($translation->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($attributes['id'])) { $metadata['id'] = (string) $attributes['id']; } $catalogue->setMetadata((string) $source, $metadata, $domain); } } } private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { $xml = simplexml_import_dom($dom); $encoding = strtoupper($dom->encoding); $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); foreach ($xml->xpath('//xliff:unit') as $unit) { foreach ($unit->segment as $segment) { $attributes = $unit->attributes(); $source = $attributes['name'] ?? $segment->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); $catalogue->set((string) $source, $target, $domain); $metadata = []; if (isset($segment->target) && $segment->target->attributes()) { $metadata['target-attributes'] = []; foreach ($segment->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($unit->notes)) { $metadata['notes'] = []; foreach ($unit->notes->note as $noteNode) { $note = []; foreach ($noteNode->attributes() as $key => $value) { $note[$key] = (string) $value; } $note['content'] = (string) $noteNode; $metadata['notes'][] = $note; } } $catalogue->setMetadata((string) $source, $metadata, $domain); } } } /** * Convert a UTF8 string to the specified encoding. */ private function utf8ToCharset(string $content, string $encoding = null): string { if ('UTF-8' !== $encoding && !empty($encoding)) { return mb_convert_encoding($content, $encoding, 'UTF-8'); } return $content; } private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array { $notes = []; if (null === $noteElement) { return $notes; } /** @var \SimpleXMLElement $xmlNote */ foreach ($noteElement as $xmlNote) { $noteAttributes = $xmlNote->attributes(); $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; if (isset($noteAttributes['priority'])) { $note['priority'] = (int) $noteAttributes['priority']; } if (isset($noteAttributes['from'])) { $note['from'] = (string) $noteAttributes['from']; } $notes[] = $note; } return $notes; } } PK!w/ҫ44Loader/IcuDatFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuDatFileLoader extends IcuResFileLoader { /** * {@inheritdoc} */ public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource.'.dat')) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource.'.dat')) { throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception $e) { $rb = null; } if (!$rb) { throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource.'.dat')); } return $catalogue; } } PK!vHLoader/MoFileLoader.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/) */ class MoFileLoader extends FileLoader { /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was little endian. */ public const MO_LITTLE_ENDIAN_MAGIC = 0x950412de; /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was big endian. */ public const MO_BIG_ENDIAN_MAGIC = 0xde120495; /** * The size of the header of an MO file in bytes. */ public const MO_HEADER_SIZE = 28; /** * Parses machine object (MO) format, independent of the machine's endian it * was created on. Both 32bit and 64bit systems are supported. * * {@inheritdoc} */ protected function loadResource($resource) { $stream = fopen($resource, 'r'); $stat = fstat($stream); if ($stat['size'] < self::MO_HEADER_SIZE) { throw new InvalidResourceException('MO stream content has an invalid format.'); } $magic = unpack('V1', fread($stream, 4)); $magic = hexdec(substr(dechex(current($magic)), -8)); if (self::MO_LITTLE_ENDIAN_MAGIC == $magic) { $isBigEndian = false; } elseif (self::MO_BIG_ENDIAN_MAGIC == $magic) { $isBigEndian = true; } else { throw new InvalidResourceException('MO stream content has an invalid format.'); } // formatRevision $this->readLong($stream, $isBigEndian); $count = $this->readLong($stream, $isBigEndian); $offsetId = $this->readLong($stream, $isBigEndian); $offsetTranslated = $this->readLong($stream, $isBigEndian); // sizeHashes $this->readLong($stream, $isBigEndian); // offsetHashes $this->readLong($stream, $isBigEndian); $messages = []; for ($i = 0; $i < $count; ++$i) { $pluralId = null; $translated = null; fseek($stream, $offsetId + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $singularId = fread($stream, $length); if (false !== strpos($singularId, "\000")) { [$singularId, $pluralId] = explode("\000", $singularId); } fseek($stream, $offsetTranslated + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $translated = fread($stream, $length); if (false !== strpos($translated, "\000")) { $translated = explode("\000", $translated); } $ids = ['singular' => $singularId, 'plural' => $pluralId]; $item = compact('ids', 'translated'); if (!empty($item['ids']['singular'])) { $id = $item['ids']['singular']; if (isset($item['ids']['plural'])) { $id .= '|'.$item['ids']['plural']; } $messages[$id] = stripcslashes(implode('|', (array) $item['translated'])); } } fclose($stream); return array_filter($messages); } /** * Reads an unsigned long from stream respecting endianness. * * @param resource $stream */ private function readLong($stream, bool $isBigEndian): int { $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4)); $result = current($result); return (int) substr($result, -8); } } PK!ȲppDataCollectorTranslator.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface { public const MESSAGE_DEFINED = 0; public const MESSAGE_MISSING = 1; public const MESSAGE_EQUALS_FALLBACK = 2; /** * @var TranslatorInterface|TranslatorBagInterface */ private $translator; private $messages = []; /** * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface */ public function __construct(TranslatorInterface $translator) { if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->collectMessage($locale, $domain, $id, $trans, $parameters); return $trans; } /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->translator->setLocale($locale); } /** * {@inheritdoc} */ public function getLocale() { return $this->translator->getLocale(); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null) { return $this->translator->getCatalogue($locale); } /** * {@inheritdoc} * * @return string[] */ public function warmUp(string $cacheDir) { if ($this->translator instanceof WarmableInterface) { return (array) $this->translator->warmUp($cacheDir); } return []; } /** * Gets the fallback locales. * * @return array The fallback locales */ public function getFallbackLocales() { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } /** * Passes through all unknown calls onto the translator object. */ public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } /** * @return array */ public function getCollectedMessages() { return $this->messages; } private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []) { if (null === $domain) { $domain = 'messages'; } $catalogue = $this->translator->getCatalogue($locale); $locale = $catalogue->getLocale(); $fallbackLocale = null; if ($catalogue->defines($id, $domain)) { $state = self::MESSAGE_DEFINED; } elseif ($catalogue->has($id, $domain)) { $state = self::MESSAGE_EQUALS_FALLBACK; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { if ($fallbackCatalogue->defines($id, $domain)) { $fallbackLocale = $fallbackCatalogue->getLocale(); break; } $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } } else { $state = self::MESSAGE_MISSING; } $this->messages[] = [ 'locale' => $locale, 'fallbackLocale' => $fallbackLocale, 'domain' => $domain, 'id' => $id, 'translation' => $translation, 'parameters' => $parameters, 'state' => $state, 'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null, ]; } } PK!?S%S%Command/XliffLintCommand.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Util\XliffUtils; /** * Validates XLIFF files syntax and outputs encountered errors. * * @author Grégoire Pineau * @author Robin Chalas * @author Javier Eguiluz */ class XliffLintCommand extends Command { protected static $defaultName = 'lint:xliff'; private $format; private $displayCorrectFiles; private $directoryIteratorProvider; private $isReadableProvider; private $requireStrictFileNames; public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true) { parent::__construct($name); $this->directoryIteratorProvider = $directoryIteratorProvider; $this->isReadableProvider = $isReadableProvider; $this->requireStrictFileNames = $requireStrictFileNames; } /** * {@inheritdoc} */ protected function configure() { $this ->setDescription('Lint an XLIFF file and outputs encountered errors') ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') ->setHelp(<<%command.name% command lints an XLIFF file and outputs to STDOUT the first encountered syntax error. You can validates XLIFF contents passed from STDIN: cat filename | php %command.full_name% - You can also validate the syntax of a file: php %command.full_name% filename Or of a whole directory: php %command.full_name% dirname php %command.full_name% dirname --format=json EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $this->format = $input->getOption('format'); $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { $filesInfo[] = $this->validate(file_get_contents($file), $file); } } return $this->display($io, $filesInfo); } private function validate(string $content, string $file = null): array { $errors = []; // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input if ('' === trim($content)) { return ['file' => $file, 'valid' => true]; } $internal = libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadXML($content); if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/')); // strict file names require translation files to be named '____.locale.xlf' // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed // also, the regexp matching must be case-insensitive, as defined for 'target-language' values // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern); if (0 === preg_match($expectedFilenamePattern, basename($file))) { $errors[] = [ 'line' => -1, 'column' => -1, 'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage), ]; } } foreach (XliffUtils::validateSchema($document) as $xmlError) { $errors[] = [ 'line' => $xmlError['line'], 'column' => $xmlError['column'], 'message' => $xmlError['message'], ]; } libxml_clear_errors(); libxml_use_internal_errors($internal); return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors]; } private function display(SymfonyStyle $io, array $files) { switch ($this->format) { case 'txt': return $this->displayTxt($io, $files); case 'json': return $this->displayJson($io, $files); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)); } } private function displayTxt(SymfonyStyle $io, array $filesInfo) { $countFiles = \count($filesInfo); $erroredFiles = 0; foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text(' ERROR '.($info['file'] ? sprintf(' in %s', $info['file']) : '')); $io->listing(array_map(function ($error) { // general document errors have a '-1' line number return -1 === $error['line'] ? $error['message'] : sprintf('Line %d, Column %d: %s', $error['line'], $error['column'], $error['message']); }, $info['messages'])); } } if (0 === $erroredFiles) { $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); } else { $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo) { $errors = 0; array_walk($filesInfo, function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } private function getFiles(string $fileOrDirectory) { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) { continue; } yield $file; } } private function getDirectoryIterator(string $directory) { $default = function ($directory) { return new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); }; if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory) { $default = function ($fileOrDirectory) { return is_readable($fileOrDirectory); }; if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string { foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) { if ('target-language' === $attribute->nodeName) { return $attribute->nodeValue; } } return null; } } PK!MessageCatalogueInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\Resource\ResourceInterface; /** * MessageCatalogueInterface. * * @author Fabien Potencier */ interface MessageCatalogueInterface { public const INTL_DOMAIN_SUFFIX = '+intl-icu'; /** * Gets the catalogue locale. * * @return string The locale */ public function getLocale(); /** * Gets the domains. * * @return array An array of domains */ public function getDomains(); /** * Gets the messages within a given domain. * * If $domain is null, it returns all messages. * * @param string $domain The domain name * * @return array An array of messages */ public function all(string $domain = null); /** * Sets a message translation. * * @param string $id The message id * @param string $translation The messages translation * @param string $domain The domain name */ public function set(string $id, string $translation, string $domain = 'messages'); /** * Checks if a message has a translation. * * @param string $id The message id * @param string $domain The domain name * * @return bool true if the message has a translation, false otherwise */ public function has(string $id, string $domain = 'messages'); /** * Checks if a message has a translation (it does not take into account the fallback mechanism). * * @param string $id The message id * @param string $domain The domain name * * @return bool true if the message has a translation, false otherwise */ public function defines(string $id, string $domain = 'messages'); /** * Gets a message translation. * * @param string $id The message id * @param string $domain The domain name * * @return string The message translation */ public function get(string $id, string $domain = 'messages'); /** * Sets translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function replace(array $messages, string $domain = 'messages'); /** * Adds translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function add(array $messages, string $domain = 'messages'); /** * Merges translations from the given Catalogue into the current one. * * The two catalogues must have the same locale. */ public function addCatalogue(self $catalogue); /** * Merges translations from the given Catalogue into the current one * only when the translation does not exist. * * This is used to provide default translations when they do not exist for the current locale. */ public function addFallbackCatalogue(self $catalogue); /** * Gets the fallback catalogue. * * @return self|null A MessageCatalogueInterface instance or null when no fallback has been set */ public function getFallbackCatalogue(); /** * Returns an array of resources loaded to build this collection. * * @return ResourceInterface[] An array of resources */ public function getResources(); /** * Adds a resource for this collection. */ public function addResource(ResourceInterface $resource); } PK!Gv>>LoggingTranslator.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Psr\Log\LoggerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var TranslatorInterface|TranslatorBagInterface */ private $translator; private $logger; /** * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface */ public function __construct(TranslatorInterface $translator, LoggerInterface $logger) { if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; $this->logger = $logger; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->log($id, $domain, $locale); return $trans; } /** * {@inheritdoc} */ public function setLocale(string $locale) { $prev = $this->translator->getLocale(); $this->translator->setLocale($locale); if ($prev === $locale) { return; } $this->logger->debug(sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale)); } /** * {@inheritdoc} */ public function getLocale() { return $this->translator->getLocale(); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null) { return $this->translator->getCatalogue($locale); } /** * Gets the fallback locales. * * @return array The fallback locales */ public function getFallbackLocales() { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } /** * Passes through all unknown calls onto the translator object. */ public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } /** * Logs for missing translations. */ private function log(string $id, ?string $domain, ?string $locale) { if (null === $domain) { $domain = 'messages'; } $catalogue = $this->translator->getCatalogue($locale); if ($catalogue->defines($id, $domain)) { return; } if ($catalogue->has($id, $domain)) { $this->logger->debug('Translation use fallback catalogue.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } else { $this->logger->warning('Translation not found.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } } } PK!8&<77Translator.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\ConfigCacheFactory; use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(MessageCatalogue::class); /** * @author Fabien Potencier */ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var MessageCatalogueInterface[] */ protected $catalogues = []; /** * @var string */ private $locale; /** * @var array */ private $fallbackLocales = []; /** * @var LoaderInterface[] */ private $loaders = []; /** * @var array */ private $resources = []; /** * @var MessageFormatterInterface */ private $formatter; /** * @var string */ private $cacheDir; /** * @var bool */ private $debug; private $cacheVary; /** * @var ConfigCacheFactoryInterface|null */ private $configCacheFactory; /** * @var array|null */ private $parentLocales; private $hasIntlFormatter; /** * @throws InvalidArgumentException If a locale contains invalid characters */ public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) { $this->setLocale($locale); if (null === $formatter) { $formatter = new MessageFormatter(); } $this->formatter = $formatter; $this->cacheDir = $cacheDir; $this->debug = $debug; $this->cacheVary = $cacheVary; $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface; } public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) { $this->configCacheFactory = $configCacheFactory; } /** * Adds a Loader. * * @param string $format The name of the loader (@see addResource()) */ public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } /** * Adds a Resource. * * @param string $format The name of the loader (@see addLoader()) * @param mixed $resource The resource name * * @throws InvalidArgumentException If the locale contains invalid characters */ public function addResource(string $format, $resource, string $locale, string $domain = null) { if (null === $domain) { $domain = 'messages'; } $this->assertValidLocale($locale); $this->resources[$locale][] = [$format, $resource, $domain]; if (\in_array($locale, $this->fallbackLocales)) { $this->catalogues = []; } else { unset($this->catalogues[$locale]); } } /** * {@inheritdoc} */ public function setLocale(string $locale) { $this->assertValidLocale($locale); $this->locale = $locale ?? (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); } /** * {@inheritdoc} */ public function getLocale() { return $this->locale; } /** * Sets the fallback locales. * * @param array $locales The fallback locales * * @throws InvalidArgumentException If a locale contains invalid characters */ public function setFallbackLocales(array $locales) { // needed as the fallback locales are linked to the already loaded catalogues $this->catalogues = []; foreach ($locales as $locale) { $this->assertValidLocale($locale); } $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales; } /** * Gets the fallback locales. * * @internal */ public function getFallbackLocales(): array { return $this->fallbackLocales; } /** * {@inheritdoc} */ public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { if (null === $id || '' === $id) { return ''; } if (null === $domain) { $domain = 'messages'; } $catalogue = $this->getCatalogue($locale); $locale = $catalogue->getLocale(); while (!$catalogue->defines($id, $domain)) { if ($cat = $catalogue->getFallbackCatalogue()) { $catalogue = $cat; $locale = $catalogue->getLocale(); } else { break; } } $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX); if ($this->hasIntlFormatter && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX) || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len))) ) { return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters); } return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters); } /** * {@inheritdoc} */ public function getCatalogue(string $locale = null) { if (null === $locale) { $locale = $this->getLocale(); } else { $this->assertValidLocale($locale); } if (!isset($this->catalogues[$locale])) { $this->loadCatalogue($locale); } return $this->catalogues[$locale]; } /** * Gets the loaders. * * @return array LoaderInterface[] */ protected function getLoaders() { return $this->loaders; } protected function loadCatalogue(string $locale) { if (null === $this->cacheDir) { $this->initializeCatalogue($locale); } else { $this->initializeCacheCatalogue($locale); } } protected function initializeCatalogue(string $locale) { $this->assertValidLocale($locale); try { $this->doLoadCatalogue($locale); } catch (NotFoundResourceException $e) { if (!$this->computeFallbackLocales($locale)) { throw $e; } } $this->loadFallbackCatalogues($locale); } private function initializeCacheCatalogue(string $locale): void { if (isset($this->catalogues[$locale])) { /* Catalogue already initialized. */ return; } $this->assertValidLocale($locale); $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale), function (ConfigCacheInterface $cache) use ($locale) { $this->dumpCatalogue($locale, $cache); } ); if (isset($this->catalogues[$locale])) { /* Catalogue has been initialized as it was written out to cache. */ return; } /* Read catalogue from cache. */ $this->catalogues[$locale] = include $cache->getPath(); } private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void { $this->initializeCatalogue($locale); $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); $content = sprintf(<<getAllMessages($this->catalogues[$locale]), true), $fallbackContent ); $cache->write($content, $this->catalogues[$locale]->getResources()); } private function getFallbackContent(MessageCatalogue $catalogue): string { $fallbackContent = ''; $current = ''; $replacementPattern = '/[^a-z0-9_]/i'; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { $fallback = $fallbackCatalogue->getLocale(); $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); $fallbackContent .= sprintf(<<<'EOF' $catalogue%s = new MessageCatalogue('%s', %s); $catalogue%s->addFallbackCatalogue($catalogue%s); EOF , $fallbackSuffix, $fallback, var_export($this->getAllMessages($fallbackCatalogue), true), $currentSuffix, $fallbackSuffix ); $current = $fallbackCatalogue->getLocale(); $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } return $fallbackContent; } private function getCatalogueCachePath(string $locale): string { return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; } /** * @internal */ protected function doLoadCatalogue(string $locale): void { $this->catalogues[$locale] = new MessageCatalogue($locale); if (isset($this->resources[$locale])) { foreach ($this->resources[$locale] as $resource) { if (!isset($this->loaders[$resource[0]])) { if (\is_string($resource[1])) { throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1])); } throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0])); } $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2])); } } } private function loadFallbackCatalogues(string $locale): void { $current = $this->catalogues[$locale]; foreach ($this->computeFallbackLocales($locale) as $fallback) { if (!isset($this->catalogues[$fallback])) { $this->initializeCatalogue($fallback); } $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback])); foreach ($this->catalogues[$fallback]->getResources() as $resource) { $fallbackCatalogue->addResource($resource); } $current->addFallbackCatalogue($fallbackCatalogue); $current = $fallbackCatalogue; } } protected function computeFallbackLocales(string $locale) { if (null === $this->parentLocales) { $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); } $locales = []; foreach ($this->fallbackLocales as $fallback) { if ($fallback === $locale) { continue; } $locales[] = $fallback; } while ($locale) { $parent = $this->parentLocales[$locale] ?? null; if ($parent) { $locale = 'root' !== $parent ? $parent : null; } elseif (\function_exists('locale_parse')) { $localeSubTags = locale_parse($locale); $locale = null; if (1 < \count($localeSubTags)) { array_pop($localeSubTags); $locale = locale_compose($localeSubTags) ?: null; } } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) { $locale = substr($locale, 0, $i); } else { $locale = null; } if (null !== $locale) { array_unshift($locales, $locale); } } return array_unique($locales); } /** * Asserts that the locale is valid, throws an Exception if not. * * @throws InvalidArgumentException If the locale contains invalid characters */ protected function assertValidLocale(string $locale) { if (1 !== preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); } } /** * Provides the ConfigCache factory implementation, falling back to a * default implementation if necessary. */ private function getConfigCacheFactory(): ConfigCacheFactoryInterface { if (!$this->configCacheFactory) { $this->configCacheFactory = new ConfigCacheFactory($this->debug); } return $this->configCacheFactory; } private function getAllMessages(MessageCatalogueInterface $catalogue): array { $allMessages = []; foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages; $messages = array_diff_key($messages, $intlMessages); } if ($messages) { $allMessages[$domain] = $messages; } } return $allMessages; } } PK!δ composer.jsonnuIw{ "name": "symfony/translation", "type": "library", "description": "Provides tools to internationalize your application", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=7.2.5", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.15", "symfony/translation-contracts": "^2.3" }, "require-dev": { "symfony/config": "^4.4|^5.0", "symfony/console": "^4.4|^5.0", "symfony/dependency-injection": "^5.0", "symfony/http-kernel": "^5.0", "symfony/intl": "^4.4|^5.0", "symfony/service-contracts": "^1.1.2|^2", "symfony/yaml": "^4.4|^5.0", "symfony/finder": "^4.4|^5.0", "psr/log": "~1.0" }, "conflict": { "symfony/config": "<4.4", "symfony/dependency-injection": "<5.0", "symfony/http-kernel": "<5.0", "symfony/twig-bundle": "<5.0", "symfony/yaml": "<4.4" }, "provide": { "symfony/translation-implementation": "2.3" }, "suggest": { "symfony/config": "", "symfony/yaml": "", "psr/log-implementation": "To use logging capability in translator" }, "autoload": { "files": [ "Resources/functions.php" ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } PK!|E 0 0 Util/ArrayConverter.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; /** * ArrayConverter generates tree like structure from a message catalogue. * e.g. this * 'foo.bar1' => 'test1', * 'foo.bar2' => 'test2' * converts to follows: * foo: * bar1: test1 * bar2: test2. * * @author Gennady Telegin */ class ArrayConverter { /** * Converts linear messages array to tree-like array. * For example this array('foo.bar' => 'value') will be converted to ['foo' => ['bar' => 'value']]. * * @param array $messages Linear messages array * * @return array Tree-like messages array */ public static function expandToTree(array $messages) { $tree = []; foreach ($messages as $id => $value) { $referenceToElement = &self::getElementByPath($tree, explode('.', $id)); $referenceToElement = $value; unset($referenceToElement); } return $tree; } private static function &getElementByPath(array &$tree, array $parts) { $elem = &$tree; $parentOfElem = null; foreach ($parts as $i => $part) { if (isset($elem[$part]) && \is_string($elem[$part])) { /* Process next case: * 'foo': 'test1', * 'foo.bar': 'test2' * * $tree['foo'] was string before we found array {bar: test2}. * Treat new element as string too, e.g. add $tree['foo.bar'] = 'test2'; */ $elem = &$elem[implode('.', \array_slice($parts, $i))]; break; } $parentOfElem = &$elem; $elem = &$elem[$part]; } if ($elem && \is_array($elem) && $parentOfElem) { /* Process next case: * 'foo.bar': 'test1' * 'foo': 'test2' * * $tree['foo'] was array = {bar: 'test1'} before we found string constant `foo`. * Cancel treating $tree['foo'] as array and cancel back it expansion, * e.g. make it $tree['foo.bar'] = 'test1' again. */ self::cancelExpand($parentOfElem, $part, $elem); } return $elem; } private static function cancelExpand(array &$tree, string $prefix, array $node) { $prefix .= '.'; foreach ($node as $id => $value) { if (\is_string($value)) { $tree[$prefix.$id] = $value; } else { self::cancelExpand($tree, $prefix.$id, $value); } } } } PK!Util/XliffUtils.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * Provides some utility methods for XLIFF translation files, such as validating * their contents according to the XSD schema. * * @author Fabien Potencier */ class XliffUtils { /** * Gets xliff file version based on the root "version" attribute. * * Defaults to 1.2 for backwards compatibility. * * @throws InvalidArgumentException */ public static function getVersionNumber(\DOMDocument $dom): string { /** @var \DOMNode $xliff */ foreach ($dom->getElementsByTagName('xliff') as $xliff) { $version = $xliff->attributes->getNamedItem('version'); if ($version) { return $version->nodeValue; } $namespace = $xliff->attributes->getNamedItem('xmlns'); if ($namespace) { if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s".', $namespace)); } return substr($namespace, 34); } } // Falls back to v1.2 return '1.2'; } /** * Validates and parses the given file into a DOMDocument. * * @throws InvalidResourceException */ public static function validateSchema(\DOMDocument $dom): array { $xliffVersion = static::getVersionNumber($dom); $internalErrors = libxml_use_internal_errors(true); if ($shouldEnable = self::shouldEnableEntityLoader()) { $disableEntities = libxml_disable_entity_loader(false); } try { $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); if (!$isValid) { return self::getXmlErrors($internalErrors); } } finally { if ($shouldEnable) { libxml_disable_entity_loader($disableEntities); } } $dom->normalizeDocument(); libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return []; } private static function shouldEnableEntityLoader(): bool { // Version prior to 8.0 can be enabled without deprecation if (\PHP_VERSION_ID < 80000) { return true; } static $dom, $schema; if (null === $dom) { $dom = new \DOMDocument(); $dom->loadXML(''); $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); register_shutdown_function(static function () use ($tmpfile) { @unlink($tmpfile); }); $schema = ' '; file_put_contents($tmpfile, ' '); } return !@$dom->schemaValidateSource($schema); } public static function getErrorsAsString(array $xmlErrors): string { $errorsAsString = ''; foreach ($xmlErrors as $error) { $errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n", \LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR', $error['code'], $error['message'], $error['file'], $error['line'], $error['column'] ); } return $errorsAsString; } private static function getSchema(string $xliffVersion): string { if ('1.2' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd'); $xmlUri = 'http://www.w3.org/2001/xml.xsd'; } elseif ('2.0' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.0.xsd'); $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; } else { throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); } return self::fixXmlLocation($schemaSource, $xmlUri); } /** * Internally changes the URI of a dependent xsd to be loaded locally. */ private static function fixXmlLocation(string $schemaSource, string $xmlUri): string { $newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd'; $parts = explode('/', $newPath); $locationstart = 'file:///'; if (0 === stripos($newPath, 'phar://')) { $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); if ($tmpfile) { copy($newPath, $tmpfile); $parts = explode('/', str_replace('\\', '/', $tmpfile)); } else { array_shift($parts); $locationstart = 'phar:///'; } } $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); return str_replace($xmlUri, $newPath, $schemaSource); } /** * Returns the XML errors of the internal XML parser. */ private static function getXmlErrors(bool $internalErrors): array { $errors = []; foreach (libxml_get_errors() as $error) { $errors[] = [ 'level' => \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', 'code' => $error->code, 'message' => trim($error->message), 'file' => $error->file ?: 'n/a', 'line' => $error->line, 'column' => $error->column, ]; } libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return $errors; } } PK!9HHTranslatableMessage.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Nate Wiebe */ class TranslatableMessage implements TranslatableInterface { private $message; private $parameters; private $domain; public function __construct(string $message, array $parameters = [], string $domain = null) { $this->message = $message; $this->parameters = $parameters; $this->domain = $domain; } public function __toString(): string { return $this->getMessage(); } public function getMessage(): string { return $this->message; } public function getParameters(): array { return $this->parameters; } public function getDomain(): ?string { return $this->domain; } public function trans(TranslatorInterface $translator, string $locale = null): string { return $translator->trans($this->getMessage(), $this->getParameters(), $this->getDomain(), $locale); } } PK!R99MetadataAwareInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; /** * MetadataAwareInterface. * * @author Fabien Potencier */ interface MetadataAwareInterface { /** * Gets metadata for the given domain and key. * * Passing an empty domain will return an array with all metadata indexed by * domain and then by key. Passing an empty key will return an array with all * metadata for the given domain. * * @param string $key The key * @param string $domain The domain name * * @return mixed The value that was set or an array with the domains/keys or null */ public function getMetadata(string $key = '', string $domain = 'messages'); /** * Adds metadata to a message domain. * * @param string $key The key * @param mixed $value The value * @param string $domain The domain name */ public function setMetadata(string $key, $value, string $domain = 'messages'); /** * Deletes metadata for the given key and domain. * * Passing an empty domain will delete all metadata. Passing an empty key will * delete all metadata for the given domain. * * @param string $key The key * @param string $domain The domain name */ public function deleteMetadata(string $key = '', string $domain = 'messages'); } PK!N &DependencyInjection/TranslatorPass.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; class TranslatorPass implements CompilerPassInterface { private $translatorServiceId; private $readerServiceId; private $loaderTag; private $debugCommandServiceId; private $updateCommandServiceId; public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update') { $this->translatorServiceId = $translatorServiceId; $this->readerServiceId = $readerServiceId; $this->loaderTag = $loaderTag; $this->debugCommandServiceId = $debugCommandServiceId; $this->updateCommandServiceId = $updateCommandServiceId; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->translatorServiceId)) { return; } $loaders = []; $loaderRefs = []; foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) { $loaderRefs[$id] = new Reference($id); $loaders[$id][] = $attributes[0]['alias']; if (isset($attributes[0]['legacy-alias'])) { $loaders[$id][] = $attributes[0]['legacy-alias']; } } if ($container->hasDefinition($this->readerServiceId)) { $definition = $container->getDefinition($this->readerServiceId); foreach ($loaders as $id => $formats) { foreach ($formats as $format) { $definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]); } } } $container ->findDefinition($this->translatorServiceId) ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) ->replaceArgument(3, $loaders) ; if (!$container->hasParameter('twig.default_path')) { return; } $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); if ($container->hasDefinition($this->debugCommandServiceId)) { $definition = $container->getDefinition($this->debugCommandServiceId); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 6) { $definition->replaceArgument(6, $paths); } } if ($container->hasDefinition($this->updateCommandServiceId)) { $definition = $container->getDefinition($this->updateCommandServiceId); $definition->replaceArgument(5, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 7) { $definition->replaceArgument(7, $paths); } } } } PK!eʱ0DependencyInjection/TranslationExtractorPass.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.extractor services to translation extractor. */ class TranslationExtractorPass implements CompilerPassInterface { private $extractorServiceId; private $extractorTag; public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor') { $this->extractorServiceId = $extractorServiceId; $this->extractorTag = $extractorTag; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->extractorServiceId)) { return; } $definition = $container->getDefinition($this->extractorServiceId); foreach ($container->findTaggedServiceIds($this->extractorTag, true) as $id => $attributes) { if (!isset($attributes[0]['alias'])) { throw new RuntimeException(sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id)); } $definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]); } } } PK!+DependencyInjection/TranslatorPathsPass.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; /** * @author Yonel Ceruto */ class TranslatorPathsPass extends AbstractRecursivePass { private $translatorServiceId; private $debugCommandServiceId; private $updateCommandServiceId; private $resolverServiceId; private $level = 0; private $paths = []; private $definitions = []; private $controllers = []; public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service') { $this->translatorServiceId = $translatorServiceId; $this->debugCommandServiceId = $debugCommandServiceId; $this->updateCommandServiceId = $updateCommandServiceId; $this->resolverServiceId = $resolverServiceId; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->translatorServiceId)) { return; } foreach ($this->findControllerArguments($container) as $controller => $argument) { $id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller)); if ($container->hasDefinition($id)) { [$locatorRef] = $argument->getValues(); $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; } } try { parent::process($container); $paths = []; foreach ($this->paths as $class => $_) { if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { $paths[] = $r->getFileName(); } } if ($paths) { if ($container->hasDefinition($this->debugCommandServiceId)) { $definition = $container->getDefinition($this->debugCommandServiceId); $definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths)); } if ($container->hasDefinition($this->updateCommandServiceId)) { $definition = $container->getDefinition($this->updateCommandServiceId); $definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths)); } } } finally { $this->level = 0; $this->paths = []; $this->definitions = []; } } protected function processValue($value, bool $isRoot = false) { if ($value instanceof Reference) { if ((string) $value === $this->translatorServiceId) { for ($i = $this->level - 1; $i >= 0; --$i) { $class = $this->definitions[$i]->getClass(); if (ServiceLocator::class === $class) { if (!isset($this->controllers[$this->currentId])) { continue; } foreach ($this->controllers[$this->currentId] as $class => $_) { $this->paths[$class] = true; } } else { $this->paths[$class] = true; } break; } } return $value; } if ($value instanceof Definition) { $this->definitions[$this->level++] = $value; $value = parent::processValue($value, $isRoot); unset($this->definitions[--$this->level]); return $value; } return parent::processValue($value, $isRoot); } private function findControllerArguments(ContainerBuilder $container): array { if ($container->hasDefinition($this->resolverServiceId)) { $argument = $container->getDefinition($this->resolverServiceId)->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } return $argument->getArgument(0); } if ($container->hasDefinition('debug.'.$this->resolverServiceId)) { $argument = $container->getDefinition('debug.'.$this->resolverServiceId)->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } $argument = $argument->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } return $argument->getArgument(0); } return []; } } PK!I*OO-DependencyInjection/TranslationDumperPass.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.formatter services to translation writer. */ class TranslationDumperPass implements CompilerPassInterface { private $writerServiceId; private $dumperTag; public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper') { $this->writerServiceId = $writerServiceId; $this->dumperTag = $dumperTag; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->writerServiceId)) { return; } $definition = $container->getDefinition($this->writerServiceId); foreach ($container->findTaggedServiceIds($this->dumperTag, true) as $id => $attributes) { $definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]); } } } PK!ϲ$Formatter/IntlFormatterInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; /** * Formats ICU message patterns. * * @author Nicolas Grekas */ interface IntlFormatterInterface { /** * Formats a localized message using rules defined by ICU MessageFormat. * * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details */ public function formatIntl(string $message, string $locale, array $parameters = []): string; } PK!Formatter/IntlFormatter.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; /** * @author Guilherme Blanco * @author Abdellatif Ait boudad */ class IntlFormatter implements IntlFormatterInterface { private $hasMessageFormatter; private $cache = []; /** * {@inheritdoc} */ public function formatIntl(string $message, string $locale, array $parameters = []): string { // MessageFormatter constructor throws an exception if the message is empty if ('' === $message) { return ''; } if (!$formatter = $this->cache[$locale][$message] ?? null) { if (!($this->hasMessageFormatter ?? $this->hasMessageFormatter = class_exists(\MessageFormatter::class))) { throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.'); } try { $this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message); } catch (\IntlException $e) { throw new InvalidArgumentException(sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e); } } foreach ($parameters as $key => $value) { if (\in_array($key[0] ?? null, ['%', '{'], true)) { unset($parameters[$key]); $parameters[trim($key, '%{ }')] = $value; } } if (false === $message = $formatter->format($parameters)) { throw new InvalidArgumentException(sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage()); } return $message; } } PK!!{{Formatter/MessageFormatter.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(IntlFormatter::class); /** * @author Abdellatif Ait boudad */ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface { private $translator; private $intlFormatter; /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ public function __construct(TranslatorInterface $translator = null, IntlFormatterInterface $intlFormatter = null) { $this->translator = $translator ?? new IdentityTranslator(); $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } /** * {@inheritdoc} */ public function format(string $message, string $locale, array $parameters = []) { if ($this->translator instanceof TranslatorInterface) { return $this->translator->trans($message, $parameters, null, $locale); } return strtr($message, $parameters); } /** * {@inheritdoc} */ public function formatIntl(string $message, string $locale, array $parameters = []): string { return $this->intlFormatter->formatIntl($message, $locale, $parameters); } } PK!h kk'Formatter/MessageFormatterInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; /** * @author Guilherme Blanco * @author Abdellatif Ait boudad */ interface MessageFormatterInterface { /** * Formats a localized message pattern with given arguments. * * @param string $message The message (may also be an object that can be cast to string) * @param string $locale The message locale * @param array $parameters An array of parameters for the message * * @return string */ public function format(string $message, string $locale, array $parameters = []); } PK! 4ppPluralizationRules.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; /** * Returns the plural rules for a given locale. * * @author Fabien Potencier * * @deprecated since Symfony 4.2, use IdentityTranslator instead */ class PluralizationRules { private static $rules = []; /** * Returns the plural position to use for the given locale and number. * * @param float $number The number * @param string $locale The locale * * @return int The plural position */ public static function get($number, $locale/*, bool $triggerDeprecation = true*/) { $number = abs($number); if (3 > \func_num_args() || func_get_arg(2)) { @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); } if ('pt_BR' === $locale) { // temporary set a locale for brazilian $locale = 'xbr'; } if (\strlen($locale) > 3) { $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); } if (isset(self::$rules[$locale])) { $return = self::$rules[$locale]($number); if (!\is_int($return) || $return < 0) { return 0; } return $return; } /* * The plural rules are derived from code of the Zend Framework (2010-09-25), * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) */ switch ($locale) { case 'az': case 'bo': case 'dz': case 'id': case 'ja': case 'jv': case 'ka': case 'km': case 'kn': case 'ko': case 'ms': case 'th': case 'tr': case 'vi': case 'zh': return 0; case 'af': case 'bn': case 'bg': case 'ca': case 'da': case 'de': case 'el': case 'en': case 'eo': case 'es': case 'et': case 'eu': case 'fa': case 'fi': case 'fo': case 'fur': case 'fy': case 'gl': case 'gu': case 'ha': case 'he': case 'hu': case 'is': case 'it': case 'ku': case 'lb': case 'ml': case 'mn': case 'mr': case 'nah': case 'nb': case 'ne': case 'nl': case 'nn': case 'no': case 'oc': case 'om': case 'or': case 'pa': case 'pap': case 'ps': case 'pt': case 'so': case 'sq': case 'sv': case 'sw': case 'ta': case 'te': case 'tk': case 'ur': case 'zu': return (1 == $number) ? 0 : 1; case 'am': case 'bh': case 'fil': case 'fr': case 'gun': case 'hi': case 'hy': case 'ln': case 'mg': case 'nso': case 'xbr': case 'ti': case 'wa': return ($number < 2) ? 0 : 1; case 'be': case 'bs': case 'hr': case 'ru': case 'sh': case 'sr': case 'uk': return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); case 'cs': case 'sk': return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); case 'ga': return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); case 'lt': return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); case 'sl': return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); case 'mk': return (1 == $number % 10) ? 0 : 1; case 'mt': return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); case 'lv': return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); case 'pl': return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); case 'cy': return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); case 'ro': return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); case 'ar': return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); default: return 0; } } /** * Overrides the default plural rule for a given locale. * * @param callable $rule A PHP callable * @param string $locale The locale */ public static function set(callable $rule, $locale) { @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); if ('pt_BR' === $locale) { // temporary set a locale for brazilian $locale = 'xbr'; } if (\strlen($locale) > 3) { $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); } self::$rules[$locale] = $rule; } } PK!C  Catalogue/OperationInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Represents an operation on catalogue(s). * * An instance of this interface performs an operation on one or more catalogues and * stores intermediate and final results of the operation. * * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and * the following results are stored: * * Messages: also called 'all', are valid messages for the given domain after the operation is performed. * * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}). * * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}). * * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'. * * @author Jean-François Simon */ interface OperationInterface { /** * Returns domains affected by operation. * * @return array */ public function getDomains(); /** * Returns all valid messages ('all') after operation. * * @return array */ public function getMessages(string $domain); /** * Returns new messages ('new') after operation. * * @return array */ public function getNewMessages(string $domain); /** * Returns obsolete messages ('obsolete') after operation. * * @return array */ public function getObsoleteMessages(string $domain); /** * Returns resulting catalogue ('result'). * * @return MessageCatalogueInterface */ public function getResult(); } PK!jNYYCatalogue/MergeOperation.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Merge operation between two catalogues as follows: * all = source ∪ target = {x: x ∈ source ∨ x ∈ target} * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅ * Basically, the result contains messages from both catalogues. * * @author Jean-François Simon */ class MergeOperation extends AbstractOperation { /** * {@inheritdoc} */ protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; foreach ($this->source->all($domain) as $id => $message) { $this->messages[$domain]['all'][$id] = $message; $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } PK!Iqn n Catalogue/TargetOperation.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Target operation between two catalogues: * intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target} * all = intersection ∪ (target ∖ intersection) = target * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = source ∖ target = {x: x ∈ source ∧ x ∉ target} * Basically, the result contains messages from the target catalogue. * * @author Michael Lee */ class TargetOperation extends AbstractOperation { /** * {@inheritdoc} */ protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; // For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``, // because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} // // For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));`` // because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback} // // For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))`` // because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} foreach ($this->source->all($domain) as $id => $message) { if ($this->target->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } else { $this->messages[$domain]['obsolete'][$id] = $message; } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } PK!_:U&Catalogue/AbstractOperation.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Base catalogues binary operation class. * * A catalogue binary operation performs operation on * source (the left argument) and target (the right argument) catalogues. * * @author Jean-François Simon */ abstract class AbstractOperation implements OperationInterface { protected $source; protected $target; protected $result; /** * @var array|null The domains affected by this operation */ private $domains; /** * This array stores 'all', 'new' and 'obsolete' messages for all valid domains. * * The data structure of this array is as follows: * * [ * 'domain 1' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * 'domain 2' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * ... * ] * * @var array The array that stores 'all', 'new' and 'obsolete' messages */ protected $messages; /** * @throws LogicException */ public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target) { if ($source->getLocale() !== $target->getLocale()) { throw new LogicException('Operated catalogues must belong to the same locale.'); } $this->source = $source; $this->target = $target; $this->result = new MessageCatalogue($source->getLocale()); $this->messages = []; } /** * {@inheritdoc} */ public function getDomains() { if (null === $this->domains) { $this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains()))); } return $this->domains; } /** * {@inheritdoc} */ public function getMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain]['all'])) { $this->processDomain($domain); } return $this->messages[$domain]['all']; } /** * {@inheritdoc} */ public function getNewMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain]['new'])) { $this->processDomain($domain); } return $this->messages[$domain]['new']; } /** * {@inheritdoc} */ public function getObsoleteMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain]['obsolete'])) { $this->processDomain($domain); } return $this->messages[$domain]['obsolete']; } /** * {@inheritdoc} */ public function getResult() { foreach ($this->getDomains() as $domain) { if (!isset($this->messages[$domain])) { $this->processDomain($domain); } } return $this->result; } /** * Performs operation on source and target catalogues for the given domain and * stores the results. * * @param string $domain The domain which the operation will be performed for */ abstract protected function processDomain(string $domain); } PK!=^22Resources/functions.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; if (!\function_exists(t::class)) { /** * @author Nate Wiebe */ function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage { return new TranslatableMessage($message, $parameters, $domain); } } PK!$Resources/bin/translation-status.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ $usageInstructions = << false, // NULL = analyze all locales 'locale_to_analyze' => null, // the reference files all the other translations are compared to 'original_files' => [ 'src/Symfony/Component/Form/Resources/translations/validators.en.xlf', 'src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf', 'src/Symfony/Component/Validator/Resources/translations/validators.en.xlf', ], ]; $argc = $_SERVER['argc']; $argv = $_SERVER['argv']; if ($argc > 3) { echo str_replace('translation-status.php', $argv[0], $usageInstructions); exit(1); } foreach (array_slice($argv, 1) as $argumentOrOption) { if (0 === strpos($argumentOrOption, '-')) { $config['verbose_output'] = true; } else { $config['locale_to_analyze'] = $argumentOrOption; } } foreach ($config['original_files'] as $originalFilePath) { if (!file_exists($originalFilePath)) { echo sprintf('The following file does not exist. Make sure that you execute this command at the root dir of the Symfony code repository.%s %s', \PHP_EOL, $originalFilePath); exit(1); } } $totalMissingTranslations = 0; foreach ($config['original_files'] as $originalFilePath) { $translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']); $translationStatus = calculateTranslationStatus($originalFilePath, $translationFilePaths); $totalMissingTranslations += array_sum(array_map(function ($translation) { return count($translation['missingKeys']); }, array_values($translationStatus))); printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output']); } exit($totalMissingTranslations > 0 ? 1 : 0); function findTranslationFiles($originalFilePath, $localeToAnalyze) { $translations = []; $translationsDir = dirname($originalFilePath); $originalFileName = basename($originalFilePath); $translationFileNamePattern = str_replace('.en.', '.*.', $originalFileName); $translationFiles = glob($translationsDir.'/'.$translationFileNamePattern, \GLOB_NOSORT); sort($translationFiles); foreach ($translationFiles as $filePath) { $locale = extractLocaleFromFilePath($filePath); if (null !== $localeToAnalyze && $locale !== $localeToAnalyze) { continue; } $translations[$locale] = $filePath; } return $translations; } function calculateTranslationStatus($originalFilePath, $translationFilePaths) { $translationStatus = []; $allTranslationKeys = extractTranslationKeys($originalFilePath); foreach ($translationFilePaths as $locale => $translationPath) { $translatedKeys = extractTranslationKeys($translationPath); $missingKeys = array_diff_key($allTranslationKeys, $translatedKeys); $translationStatus[$locale] = [ 'total' => count($allTranslationKeys), 'translated' => count($translatedKeys), 'missingKeys' => $missingKeys, ]; } return $translationStatus; } function printTranslationStatus($originalFilePath, $translationStatus, $verboseOutput) { printTitle($originalFilePath); printTable($translationStatus, $verboseOutput); echo \PHP_EOL.\PHP_EOL; } function extractLocaleFromFilePath($filePath) { $parts = explode('.', $filePath); return $parts[count($parts) - 2]; } function extractTranslationKeys($filePath) { $translationKeys = []; $contents = new \SimpleXMLElement(file_get_contents($filePath)); foreach ($contents->file->body->{'trans-unit'} as $translationKey) { $translationId = (string) $translationKey['id']; $translationKey = (string) $translationKey->source; $translationKeys[$translationId] = $translationKey; } return $translationKeys; } function printTitle($title) { echo $title.\PHP_EOL; echo str_repeat('=', strlen($title)).\PHP_EOL.\PHP_EOL; } function printTable($translations, $verboseOutput) { if (0 === count($translations)) { echo 'No translations found'; return; } $longestLocaleNameLength = max(array_map('strlen', array_keys($translations))); foreach ($translations as $locale => $translation) { if ($translation['translated'] > $translation['total']) { textColorRed(); } elseif ($translation['translated'] === $translation['total']) { textColorGreen(); } echo sprintf('| Locale: %-'.$longestLocaleNameLength.'s | Translated: %d/%d', $locale, $translation['translated'], $translation['total']).\PHP_EOL; textColorNormal(); if (true === $verboseOutput && count($translation['missingKeys']) > 0) { echo str_repeat('-', 80).\PHP_EOL; echo '| Missing Translations:'.\PHP_EOL; foreach ($translation['missingKeys'] as $id => $content) { echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL; } echo str_repeat('-', 80).\PHP_EOL; } } } function textColorGreen() { echo "\033[32m"; } function textColorRed() { echo "\033[31m"; } function textColorNormal() { echo "\033[0m"; } PK!; ; Resources/data/parents.jsonnuIw{ "az_Cyrl": "root", "bs_Cyrl": "root", "en_150": "en_001", "en_AG": "en_001", "en_AI": "en_001", "en_AT": "en_150", "en_AU": "en_001", "en_BB": "en_001", "en_BE": "en_150", "en_BM": "en_001", "en_BS": "en_001", "en_BW": "en_001", "en_BZ": "en_001", "en_CA": "en_001", "en_CC": "en_001", "en_CH": "en_150", "en_CK": "en_001", "en_CM": "en_001", "en_CX": "en_001", "en_CY": "en_001", "en_DE": "en_150", "en_DG": "en_001", "en_DK": "en_150", "en_DM": "en_001", "en_ER": "en_001", "en_FI": "en_150", "en_FJ": "en_001", "en_FK": "en_001", "en_FM": "en_001", "en_GB": "en_001", "en_GD": "en_001", "en_GG": "en_001", "en_GH": "en_001", "en_GI": "en_001", "en_GM": "en_001", "en_GY": "en_001", "en_HK": "en_001", "en_IE": "en_001", "en_IL": "en_001", "en_IM": "en_001", "en_IN": "en_001", "en_IO": "en_001", "en_JE": "en_001", "en_JM": "en_001", "en_KE": "en_001", "en_KI": "en_001", "en_KN": "en_001", "en_KY": "en_001", "en_LC": "en_001", "en_LR": "en_001", "en_LS": "en_001", "en_MG": "en_001", "en_MO": "en_001", "en_MS": "en_001", "en_MT": "en_001", "en_MU": "en_001", "en_MW": "en_001", "en_MY": "en_001", "en_NA": "en_001", "en_NF": "en_001", "en_NG": "en_001", "en_NL": "en_150", "en_NR": "en_001", "en_NU": "en_001", "en_NZ": "en_001", "en_PG": "en_001", "en_PH": "en_001", "en_PK": "en_001", "en_PN": "en_001", "en_PW": "en_001", "en_RW": "en_001", "en_SB": "en_001", "en_SC": "en_001", "en_SD": "en_001", "en_SE": "en_150", "en_SG": "en_001", "en_SH": "en_001", "en_SI": "en_150", "en_SL": "en_001", "en_SS": "en_001", "en_SX": "en_001", "en_SZ": "en_001", "en_TC": "en_001", "en_TK": "en_001", "en_TO": "en_001", "en_TT": "en_001", "en_TV": "en_001", "en_TZ": "en_001", "en_UG": "en_001", "en_VC": "en_001", "en_VG": "en_001", "en_VU": "en_001", "en_WS": "en_001", "en_ZA": "en_001", "en_ZM": "en_001", "en_ZW": "en_001", "es_AR": "es_419", "es_BO": "es_419", "es_BR": "es_419", "es_BZ": "es_419", "es_CL": "es_419", "es_CO": "es_419", "es_CR": "es_419", "es_CU": "es_419", "es_DO": "es_419", "es_EC": "es_419", "es_GT": "es_419", "es_HN": "es_419", "es_MX": "es_419", "es_NI": "es_419", "es_PA": "es_419", "es_PE": "es_419", "es_PR": "es_419", "es_PY": "es_419", "es_SV": "es_419", "es_US": "es_419", "es_UY": "es_419", "es_VE": "es_419", "ff_Adlm": "root", "pa_Arab": "root", "pt_AO": "pt_PT", "pt_CH": "pt_PT", "pt_CV": "pt_PT", "pt_GQ": "pt_PT", "pt_GW": "pt_PT", "pt_LU": "pt_PT", "pt_MO": "pt_PT", "pt_MZ": "pt_PT", "pt_ST": "pt_PT", "pt_TL": "pt_PT", "sd_Deva": "root", "sr_Latn": "root", "uz_Arab": "root", "uz_Cyrl": "root", "zh_Hant": "root", "zh_Hant_MO": "zh_Hant_HK" } PK!a+Resources/schemas/xliff-core-1.2-strict.xsdnuIw Values for the attribute 'context-type'. Indicates a database content. Indicates the content of an element within an XML document. Indicates the name of an element within an XML document. Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found. Indicates a the number of parameters contained within the <source>. Indicates notes pertaining to the parameters in the <source>. Indicates the content of a record within a database. Indicates the name of a record within a database. Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file. Values for the attribute 'count-type'. Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts. Indicates the count units are translation units existing already in the same document. Indicates a total count. Values for the attribute 'ctype' when used other elements than <ph> or <x>. Indicates a run of bolded text. Indicates a run of text in italics. Indicates a run of underlined text. Indicates a run of hyper-text. Values for the attribute 'ctype' when used with <ph> or <x>. Indicates a inline image. Indicates a page break. Indicates a line break. Values for the attribute 'datatype'. Indicates Active Server Page data. Indicates C source file data. Indicates Channel Definition Format (CDF) data. Indicates ColdFusion data. Indicates C++ source file data. Indicates C-Sharp data. Indicates strings from C, ASM, and driver files data. Indicates comma-separated values data. Indicates database data. Indicates portions of document that follows data and contains metadata. Indicates portions of document that precedes data and contains metadata. Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import). Indicates standard user input screen data. Indicates HyperText Markup Language (HTML) data - document instance. Indicates content within an HTML document’s <body> element. Indicates Windows INI file data. Indicates Interleaf data. Indicates Java source file data (extension '.java'). Indicates Java property resource bundle data. Indicates Java list resource bundle data. Indicates JavaScript source file data. Indicates JScript source file data. Indicates information relating to formatting. Indicates LISP source file data. Indicates information relating to margin formats. Indicates a file containing menu. Indicates numerically identified string table. Indicates Maker Interchange Format (MIF) data. Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute. Indicates GNU Machine Object data. Indicates Message Librarian strings created by Novell's Message Librarian Tool. Indicates information to be displayed at the bottom of each page of a document. Indicates information to be displayed at the top of each page of a document. Indicates a list of property values (e.g., settings within INI files or preferences dialog). Indicates Pascal source file data. Indicates Hypertext Preprocessor data. Indicates plain text file (no formatting other than, possibly, wrapping). Indicates GNU Portable Object file. Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc. Indicates Windows .NET binary resources. Indicates Windows .NET Resources. Indicates Rich Text Format (RTF) data. Indicates Standard Generalized Markup Language (SGML) data - document instance. Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD). Indicates Scalable Vector Graphic (SVG) data. Indicates VisualBasic Script source file. Indicates warning message. Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file). Indicates Extensible HyperText Markup Language (XHTML) data - document instance. Indicates Extensible Markup Language (XML) data - document instance. Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD). Indicates Extensible Stylesheet Language (XSL) data. Indicates XUL elements. Values for the attribute 'mtype'. Indicates the marked text is an abbreviation. ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept. ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective'). ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging'). ISO-12620: A proper-name term, such as the name of an agency or other proper entity. ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another. ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language. Indicates the marked text is a date and/or time. ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign. ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form. ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula. ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record. ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy'). ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body. ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages. ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like. ISO-12620 2.1.17: A unit to track object. Indicates the marked text is a name. ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others. ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system. Indicates the marked text is a phrase. ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase. Indicates the marked text should not be translated. ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet. Indicates that the marked text represents a segment. ISO-12620 2.1.18.2: A fixed, lexicalized phrase. ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs'). ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system. ISO-12620 2.1.19: A fixed chunk of recurring text. ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof. ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry. ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language. Indicates the marked text is a term. ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted. ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system. ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza'). ISO-12620 2.1.9: One of the alternate forms of a term. Values for the attribute 'restype'. Indicates a Windows RC AUTO3STATE control. Indicates a Windows RC AUTOCHECKBOX control. Indicates a Windows RC AUTORADIOBUTTON control. Indicates a Windows RC BEDIT control. Indicates a bitmap, for example a BITMAP resource in Windows. Indicates a button object, for example a BUTTON control Windows. Indicates a caption, such as the caption of a dialog box. Indicates the cell in a table, for example the content of the <td> element in HTML. Indicates check box object, for example a CHECKBOX control in Windows. Indicates a menu item with an associated checkbox. Indicates a list box, but with a check-box for each item. Indicates a color selection dialog. Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows. Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234). Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403). Indicates a UI base class element that cannot be represented by any other element. Indicates a context menu. Indicates a Windows RC CTEXT control. Indicates a cursor, for example a CURSOR resource in Windows. Indicates a date/time picker. Indicates a Windows RC DEFPUSHBUTTON control. Indicates a dialog box. Indicates a Windows RC DLGINIT resource block. Indicates an edit box object, for example an EDIT control in Windows. Indicates a filename. Indicates a file dialog. Indicates a footnote. Indicates a font name. Indicates a footer. Indicates a frame object. Indicates a XUL grid element. Indicates a groupbox object, for example a GROUPBOX control in Windows. Indicates a header item. Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML. Indicates a Windows RC HEDIT control. Indicates a horizontal scrollbar. Indicates an icon, for example an ICON resource in Windows. Indicates a Windows RC IEDIT control. Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF. Indicates a label object. Indicates a label that is also a HTML link (not necessarily a URL). Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML). Indicates a listbox object, for example an LISTBOX control in Windows. Indicates an list item (an entry in a list). Indicates a Windows RC LTEXT control. Indicates a menu (a group of menu-items). Indicates a toolbar containing one or more tope level menus. Indicates a menu item (an entry in a menu). Indicates a XUL menuseparator element. Indicates a message, for example an entry in a MESSAGETABLE resource in Windows. Indicates a calendar control. Indicates an edit box beside a spin control. Indicates a catch all for rectangular areas. Indicates a standalone menu not necessarily associated with a menubar. Indicates a pushbox object, for example a PUSHBOX control in Windows. Indicates a Windows RC PUSHBUTTON control. Indicates a radio button object. Indicates a menuitem with associated radio button. Indicates raw data resources for an application. Indicates a row in a table. Indicates a Windows RC RTEXT control. Indicates a user navigable container used to show a portion of a document. Indicates a generic divider object (e.g. menu group separator). Windows accelerators, shortcuts in resource or property files. Indicates a UI control to indicate process activity but not progress. Indicates a splitter bar. Indicates a Windows RC STATE3 control. Indicates a window for providing feedback to the users, like 'read-only', etc. Indicates a string, for example an entry in a STRINGTABLE resource in Windows. Indicates a layers of controls with a tab to select layers. Indicates a display and edits regular two-dimensional tables of cells. Indicates a XUL textbox element. Indicates a UI button that can be toggled to on or off state. Indicates an array of controls, usually buttons. Indicates a pop up tool tip text. Indicates a bar with a pointer indicating a position within a certain range. Indicates a control that displays a set of hierarchical data. Indicates a URI (URN or URL). Indicates a Windows RC USERBUTTON control. Indicates a user-defined control like CONTROL control in Windows. Indicates the text of a variable. Indicates version information about a resource like VERSIONINFO in Windows. Indicates a vertical scrollbar. Indicates a graphical window. Values for the attribute 'size-unit'. Indicates a size in 8-bit bytes. Indicates a size in Unicode characters. Indicates a size in columns. Used for HTML text area. Indicates a size in centimeters. Indicates a size in dialog units, as defined in Windows resources. Indicates a size in 'font-size' units (as defined in CSS). Indicates a size in 'x-height' units (as defined in CSS). Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster' Indicates a size in inches. Indicates a size in millimeters. Indicates a size in percentage. Indicates a size in pixels. Indicates a size in point. Indicates a size in rows. Used for HTML text area. Values for the attribute 'state'. Indicates the terminating state. Indicates only non-textual information needs adaptation. Indicates both text and non-textual information needs adaptation. Indicates only non-textual information needs review. Indicates both text and non-textual information needs review. Indicates that only the text of the item needs to be reviewed. Indicates that the item needs to be translated. Indicates that the item is new. For example, translation units that were not in a previous version of the document. Indicates that changes are reviewed and approved. Indicates that the item has been translated. Values for the attribute 'state-qualifier'. Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously. Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.). Indicates a match based on matching IDs (in addition to matching text). Indicates a translation derived from a glossary. Indicates a translation derived from existing translation. Indicates a translation derived from machine translation. Indicates a translation derived from a translation repository. Indicates a translation derived from a translation memory. Indicates the translation is suggested by machine translation. Indicates that the item has been rejected because of incorrect grammar. Indicates that the item has been rejected because it is incorrect. Indicates that the item has been rejected because it is too long or too short. Indicates that the item has been rejected because of incorrect spelling. Indicates the translation is suggested by translation memory. Values for the attribute 'unit'. Refers to words. Refers to pages. Refers to <trans-unit> elements. Refers to <bin-unit> elements. Refers to glyphs. Refers to <trans-unit> and/or <bin-unit> elements. Refers to the occurrences of instances defined by the count-type value. Refers to characters. Refers to lines. Refers to sentences. Refers to paragraphs. Refers to segments. Refers to placeables (inline elements). Values for the attribute 'priority'. Highest priority. High priority. High priority, but not as important as 2. High priority, but not as important as 3. Medium priority, but more important than 6. Medium priority, but less important than 5. Low priority, but more important than 8. Low priority, but more important than 9. Low priority. Lowest priority. This value indicates that all properties can be reformatted. This value must be used alone. This value indicates that no properties should be reformatted. This value must be used alone. This value indicates that all information in the coord attribute can be modified. This value indicates that the x information in the coord attribute can be modified. This value indicates that the y information in the coord attribute can be modified. This value indicates that the cx information in the coord attribute can be modified. This value indicates that the cy information in the coord attribute can be modified. This value indicates that all the information in the font attribute can be modified. This value indicates that the name information in the font attribute can be modified. This value indicates that the size information in the font attribute can be modified. This value indicates that the weight information in the font attribute can be modified. This value indicates that the information in the css-style attribute can be modified. This value indicates that the information in the style attribute can be modified. This value indicates that the information in the exstyle attribute can be modified. Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document. Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed. Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed. Represents a translation proposal from a translation memory or other resource. Represents a previous version of the target element. Represents a rejected version of the target element. Represents a translation to be used for reference purposes only, for example from a related product or a different language. Represents a proposed translation that was used for the translation of the trans-unit, possibly modified. Values for the attribute 'coord'. Version values: 1.0 and 1.1 are allowed for backward compatibility. PK!DlAlA$Resources/schemas/xliff-core-2.0.xsdnuIw PK!"@""Resources/schemas/xml.xsdnuIw

About the XML namespace

This schema document describes the XML namespace, in a form suitable for import by other schema documents.

See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace.

Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance.

See further below in this document for more information about how to refer to this schema document from your own XSD schema documents and about the namespace-versioning policy governing this schema document.

lang (as an attribute name)

denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

Notes

Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility.

See BCP 47 at http://www.rfc-editor.org/rfc/bcp/bcp47.txt and the IANA language subtag registry at http://www.iana.org/assignments/language-subtag-registry for further information.

The union allows for the 'un-declaration' of xml:lang with the empty string.

space (as an attribute name)

denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

base (as an attribute name)

denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.

See http://www.w3.org/TR/xmlbase/ for information about this attribute.

id (as an attribute name)

denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.

See http://www.w3.org/TR/xml-id/ for information about this attribute.

Father (in any context at all)

denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups:

In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father".

About this schema document

This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define.

To enable this, such a schema must import this schema for the XML namespace, e.g. as follows:

          <schema.. .>
          .. .
           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
     

or


           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
     

Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g.

          <type.. .>
          .. .
           <attributeGroup ref="xml:specialAttrs"/>
     

will define a type which will schema-validate an instance element with any of those attributes.

Versioning policy for this schema document

In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2009/01/xml.xsd.

At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd.

The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2009/01/xml.xsd will not change.

Previous dated (and unchanging) versions of this schema document are at:

PK!?[ Exception/ExceptionInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier */ interface ExceptionInterface extends \Throwable { } PK! &Exception/InvalidResourceException.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource cannot be loaded. * * @author Fabien Potencier */ class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface { } PK![NException/RuntimeException.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base RuntimeException for the Translation component. * * @author Abdellatif Ait boudad */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } PK!GP'Exception/NotFoundResourceException.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource does not exist. * * @author Fabien Potencier */ class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface { } PK!CaException/LogicException.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base LogicException for Translation component. * * @author Abdellatif Ait boudad */ class LogicException extends \LogicException implements ExceptionInterface { } PK!>O  &Exception/InvalidArgumentException.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base InvalidArgumentException for the Translation component. * * @author Abdellatif Ait boudad */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } PK!.!iH#H#MessageCatalogue.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Translation\Exception\LogicException; /** * @author Fabien Potencier */ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface { private $messages = []; private $metadata = []; private $resources = []; private $locale; private $fallbackCatalogue; private $parent; /** * @param string $locale The locale * @param array $messages An array of messages classified by domain */ public function __construct(string $locale, array $messages = []) { $this->locale = $locale; $this->messages = $messages; } /** * {@inheritdoc} */ public function getLocale() { return $this->locale; } /** * {@inheritdoc} */ public function getDomains() { $domains = []; $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX); foreach ($this->messages as $domain => $messages) { if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) { $domain = substr($domain, 0, $i); } $domains[$domain] = $domain; } return array_values($domains); } /** * {@inheritdoc} */ public function all(string $domain = null) { if (null !== $domain) { // skip messages merge if intl-icu requested explicitly if (false !== strpos($domain, self::INTL_DOMAIN_SUFFIX)) { return $this->messages[$domain] ?? []; } return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []); } $allMessages = []; $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX); foreach ($this->messages as $domain => $messages) { if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) { $domain = substr($domain, 0, $i); $allMessages[$domain] = $messages + ($allMessages[$domain] ?? []); } else { $allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages; } } return $allMessages; } /** * {@inheritdoc} */ public function set(string $id, string $translation, string $domain = 'messages') { $this->add([$id => $translation], $domain); } /** * {@inheritdoc} */ public function has(string $id, string $domain = 'messages') { if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return true; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->has($id, $domain); } return false; } /** * {@inheritdoc} */ public function defines(string $id, string $domain = 'messages') { return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); } /** * {@inheritdoc} */ public function get(string $id, string $domain = 'messages') { if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; } if (isset($this->messages[$domain][$id])) { return $this->messages[$domain][$id]; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->get($id, $domain); } return $id; } /** * {@inheritdoc} */ public function replace(array $messages, string $domain = 'messages') { unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); $this->add($messages, $domain); } /** * {@inheritdoc} */ public function add(array $messages, string $domain = 'messages') { if (!isset($this->messages[$domain])) { $this->messages[$domain] = []; } $intlDomain = $domain; $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX); if (\strlen($domain) < $suffixLength || false === strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) { $intlDomain .= self::INTL_DOMAIN_SUFFIX; } foreach ($messages as $id => $message) { if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) { $this->messages[$intlDomain][$id] = $message; } else { $this->messages[$domain][$id] = $message; } } } /** * {@inheritdoc} */ public function addCatalogue(MessageCatalogueInterface $catalogue) { if ($catalogue->getLocale() !== $this->locale) { throw new LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale)); } foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) { $this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX); $messages = array_diff_key($messages, $intlMessages); } $this->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } if ($catalogue instanceof MetadataAwareInterface) { $metadata = $catalogue->getMetadata('', ''); $this->addMetadata($metadata); } } /** * {@inheritdoc} */ public function addFallbackCatalogue(MessageCatalogueInterface $catalogue) { // detect circular references $c = $catalogue; while ($c = $c->getFallbackCatalogue()) { if ($c->getLocale() === $this->getLocale()) { throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } } $c = $this; do { if ($c->getLocale() === $catalogue->getLocale()) { throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } foreach ($catalogue->getResources() as $resource) { $c->addResource($resource); } } while ($c = $c->parent); $catalogue->parent = $this; $this->fallbackCatalogue = $catalogue; foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } } /** * {@inheritdoc} */ public function getFallbackCatalogue() { return $this->fallbackCatalogue; } /** * {@inheritdoc} */ public function getResources() { return array_values($this->resources); } /** * {@inheritdoc} */ public function addResource(ResourceInterface $resource) { $this->resources[$resource->__toString()] = $resource; } /** * {@inheritdoc} */ public function getMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { return $this->metadata; } if (isset($this->metadata[$domain])) { if ('' == $key) { return $this->metadata[$domain]; } if (isset($this->metadata[$domain][$key])) { return $this->metadata[$domain][$key]; } } return null; } /** * {@inheritdoc} */ public function setMetadata(string $key, $value, string $domain = 'messages') { $this->metadata[$domain][$key] = $value; } /** * {@inheritdoc} */ public function deleteMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { $this->metadata = []; } elseif ('' == $key) { unset($this->metadata[$domain]); } else { unset($this->metadata[$domain][$key]); } } /** * Adds current values with the new values. * * @param array $values Values to add */ private function addMetadata(array $values) { foreach ($values as $domain => $keys) { foreach ($keys as $key => $value) { $this->setMetadata($key, $value, $domain); } } } } PK!pF*DataCollector/TranslationDataCollector.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Abdellatif Ait boudad * * @final */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { private $translator; public function __construct(DataCollectorTranslator $translator) { $this->translator = $translator; } /** * {@inheritdoc} */ public function lateCollect() { $messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); $this->data += $this->computeCount($messages); $this->data['messages'] = $messages; $this->data = $this->cloneVar($this->data); } /** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['locale'] = $this->translator->getLocale(); $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); } /** * {@inheritdoc} */ public function reset() { $this->data = []; } /** * @return array|Data */ public function getMessages() { return $this->data['messages'] ?? []; } /** * @return int */ public function getCountMissings() { return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0; } /** * @return int */ public function getCountFallbacks() { return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0; } /** * @return int */ public function getCountDefines() { return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0; } public function getLocale() { return !empty($this->data['locale']) ? $this->data['locale'] : null; } /** * @internal */ public function getFallbackLocales() { return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : []; } /** * {@inheritdoc} */ public function getName() { return 'translation'; } private function sanitizeCollectedMessages(array $messages) { $result = []; foreach ($messages as $key => $message) { $messageId = $message['locale'].$message['domain'].$message['id']; if (!isset($result[$messageId])) { $message['count'] = 1; $message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : []; $messages[$key]['translation'] = $this->sanitizeString($message['translation']); $result[$messageId] = $message; } else { if (!empty($message['parameters'])) { $result[$messageId]['parameters'][] = $message['parameters']; } ++$result[$messageId]['count']; } unset($messages[$key]); } return $result; } private function computeCount(array $messages) { $count = [ DataCollectorTranslator::MESSAGE_DEFINED => 0, DataCollectorTranslator::MESSAGE_MISSING => 0, DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, ]; foreach ($messages as $message) { ++$count[$message['state']]; } return $count; } private function sanitizeString(string $string, int $length = 80) { $string = trim(preg_replace('/\s+/', ' ', $string)); if (false !== $encoding = mb_detect_encoding($string, null, true)) { if (mb_strlen($string, $encoding) > $length) { return mb_substr($string, 0, $length - 3, $encoding).'...'; } } elseif (\strlen($string) > $length) { return substr($string, 0, $length - 3).'...'; } return $string; } } PK![Q3IdentityTranslator.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; /** * IdentityTranslator does not translate anything. * * @author Fabien Potencier */ class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface { use TranslatorTrait; } PK!B))TranslatorBagInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * TranslatorBagInterface. * * @author Abdellatif Ait boudad */ interface TranslatorBagInterface { /** * Gets the catalogue by locale. * * @param string|null $locale The locale or null to use the default * * @return MessageCatalogueInterface * * @throws InvalidArgumentException If the locale contains invalid characters */ public function getCatalogue(string $locale = null); } PK!** PseudoLocalizationTranslator.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Contracts\Translation\TranslatorInterface; /** * This translator should only be used in a development environment. */ final class PseudoLocalizationTranslator implements TranslatorInterface { private const EXPANSION_CHARACTER = '~'; private $translator; private $accents; private $expansionFactor; private $brackets; private $parseHTML; private $localizableHTMLAttributes; /** * Available options: * * accents: * type: boolean * default: true * description: replace ASCII characters of the translated string with accented versions or similar characters * example: if true, "foo" => "ƒöö". * * * expansion_factor: * type: float * default: 1 * validation: it must be greater than or equal to 1 * description: expand the translated string by the given factor with spaces and tildes * example: if 2, "foo" => "~foo ~" * * * brackets: * type: boolean * default: true * description: wrap the translated string with brackets * example: if true, "foo" => "[foo]" * * * parse_html: * type: boolean * default: false * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo
bar" => "foo
bar
" * * * localizable_html_attributes: * type: string[] * default: [] * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true * example: if ["title"], and with the "accents" option set to true, "Profile" => "Þŕöƒîļé" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. */ public function __construct(TranslatorInterface $translator, array $options = []) { $this->translator = $translator; $this->accents = $options['accents'] ?? true; if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); } $this->brackets = $options['brackets'] ?? true; $this->parseHTML = $options['parse_html'] ?? false; if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { $this->parseHTML = false; } $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; } /** * {@inheritdoc} */ public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null) { $trans = ''; $visibleText = ''; foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { if ($visible) { $visibleText .= $text; } if (!$localizable) { $trans .= $text; continue; } $this->addAccents($trans, $text); } $this->expand($trans, $visibleText); $this->addBrackets($trans); return $trans; } private function getParts(string $originalTrans): array { if (!$this->parseHTML) { return [[true, true, $originalTrans]]; } $html = mb_convert_encoding($originalTrans, 'HTML-ENTITIES', mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); $useInternalErrors = libxml_use_internal_errors(true); $dom = new \DOMDocument(); $dom->loadHTML(''.$html.''); libxml_clear_errors(); libxml_use_internal_errors($useInternalErrors); return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); } private function parseNode(\DOMNode $node): array { $parts = []; foreach ($node->childNodes as $childNode) { if (!$childNode instanceof \DOMElement) { $parts[] = [true, true, $childNode->nodeValue]; continue; } $parts[] = [false, false, '<'.$childNode->tagName]; /** @var \DOMAttr $attribute */ foreach ($childNode->attributes as $attribute) { $parts[] = [false, false, ' '.$attribute->nodeName.'="']; $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { if ('' === $match) { continue; } $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; } $parts[] = [false, false, '"']; } $parts[] = [false, false, '>']; $parts = array_merge($parts, $this->parseNode($childNode, $parts)); $parts[] = [false, false, 'tagName.'>']; } return $parts; } private function addAccents(string &$trans, string $text): void { $trans .= $this->accents ? strtr($text, [ ' ' => ' ', '!' => '¡', '"' => '″', '#' => '♯', '$' => '€', '%' => '‰', '&' => '⅋', '\'' => '´', '(' => '{', ')' => '}', '*' => '⁎', '+' => '⁺', ',' => '،', '-' => '‐', '.' => '·', '/' => '⁄', '0' => '⓪', '1' => '①', '2' => '②', '3' => '③', '4' => '④', '5' => '⑤', '6' => '⑥', '7' => '⑦', '8' => '⑧', '9' => '⑨', ':' => '∶', ';' => '⁏', '<' => '≤', '=' => '≂', '>' => '≥', '?' => '¿', '@' => '՞', 'A' => 'Å', 'B' => 'Ɓ', 'C' => 'Ç', 'D' => 'Ð', 'E' => 'É', 'F' => 'Ƒ', 'G' => 'Ĝ', 'H' => 'Ĥ', 'I' => 'Î', 'J' => 'Ĵ', 'K' => 'Ķ', 'L' => 'Ļ', 'M' => 'Ṁ', 'N' => 'Ñ', 'O' => 'Ö', 'P' => 'Þ', 'Q' => 'Ǫ', 'R' => 'Ŕ', 'S' => 'Š', 'T' => 'Ţ', 'U' => 'Û', 'V' => 'Ṽ', 'W' => 'Ŵ', 'X' => 'Ẋ', 'Y' => 'Ý', 'Z' => 'Ž', '[' => '⁅', '\\' => '∖', ']' => '⁆', '^' => '˄', '_' => '‿', '`' => '‵', 'a' => 'å', 'b' => 'ƀ', 'c' => 'ç', 'd' => 'ð', 'e' => 'é', 'f' => 'ƒ', 'g' => 'ĝ', 'h' => 'ĥ', 'i' => 'î', 'j' => 'ĵ', 'k' => 'ķ', 'l' => 'ļ', 'm' => 'ɱ', 'n' => 'ñ', 'o' => 'ö', 'p' => 'þ', 'q' => 'ǫ', 'r' => 'ŕ', 's' => 'š', 't' => 'ţ', 'u' => 'û', 'v' => 'ṽ', 'w' => 'ŵ', 'x' => 'ẋ', 'y' => 'ý', 'z' => 'ž', '{' => '(', '|' => '¦', '}' => ')', '~' => '˞', ]) : $text; } private function expand(string &$trans, string $visibleText): void { if (1.0 >= $this->expansionFactor) { return; } $visibleLength = $this->strlen($visibleText); $missingLength = (int) (ceil($visibleLength * $this->expansionFactor)) - $visibleLength; if ($this->brackets) { $missingLength -= 2; } if (0 >= $missingLength) { return; } $words = []; $wordsCount = 0; foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { $wordLength = $this->strlen($word); if ($wordLength >= $missingLength) { continue; } if (!isset($words[$wordLength])) { $words[$wordLength] = 0; } ++$words[$wordLength]; ++$wordsCount; } if (!$words) { $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); return; } arsort($words, \SORT_NUMERIC); $longestWordLength = max(array_keys($words)); while (true) { $r = mt_rand(1, $wordsCount); foreach ($words as $length => $count) { $r -= $count; if ($r <= 0) { break; } } $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); $missingLength -= $length + 1; if (0 === $missingLength) { return; } while ($longestWordLength >= $missingLength) { $wordsCount -= $words[$longestWordLength]; unset($words[$longestWordLength]); if (!$words) { $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); return; } $longestWordLength = max(array_keys($words)); } } } private function addBrackets(string &$trans): void { if (!$this->brackets) { return; } $trans = '['.$trans.']'; } private function strlen(string $s): int { return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); } } PK!vu Dumper/IcuResFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue. * * @author Stealth35 */ class IcuResFileDumper extends FileDumper { /** * {@inheritdoc} */ protected $relativePathTemplate = '%domain%/%locale%.%extension%'; /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $data = $indexes = $resources = ''; foreach ($messages->all($domain) as $source => $target) { $indexes .= pack('v', \strlen($data) + 28); $data .= $source."\0"; } $data .= $this->writePadding($data); $keyTop = $this->getPosition($data); foreach ($messages->all($domain) as $source => $target) { $resources .= pack('V', $this->getPosition($data)); $data .= pack('V', \strlen($target)) .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8') .$this->writePadding($data) ; } $resOffset = $this->getPosition($data); $data .= pack('v', \count($messages->all($domain))) .$indexes .$this->writePadding($data) .$resources ; $bundleTop = $this->getPosition($data); $root = pack('V7', $resOffset + (2 << 28), // Resource Offset + Resource Type 6, // Index length $keyTop, // Index keys top $bundleTop, // Index resources top $bundleTop, // Index bundle top \count($messages->all($domain)), // Index max table length 0 // Index attributes ); $header = pack('vC2v4C12@32', 32, // Header size 0xDA, 0x27, // Magic number 1 and 2 20, 0, 0, 2, // Rest of the header, ..., Size of a char 0x52, 0x65, 0x73, 0x42, // Data format identifier 1, 2, 0, 0, // Data version 1, 4, 0, 0 // Unicode version ); return $header.$root.$data; } private function writePadding(string $data): ?string { $padding = \strlen($data) % 4; return $padding ? str_repeat("\xAA", 4 - $padding) : null; } private function getPosition(string $data) { return (\strlen($data) + 28) / 4; } /** * {@inheritdoc} */ protected function getExtension() { return 'res'; } } PK!o`R~~Dumper/CsvFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * CsvFileDumper generates a csv formatted string representation of a message catalogue. * * @author Stealth35 */ class CsvFileDumper extends FileDumper { private $delimiter = ';'; private $enclosure = '"'; /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $handle = fopen('php://memory', 'r+'); foreach ($messages->all($domain) as $source => $target) { fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure); } rewind($handle); $output = stream_get_contents($handle); fclose($handle); return $output; } /** * Sets the delimiter and escape character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; } /** * {@inheritdoc} */ protected function getExtension() { return 'csv'; } } PK!0H# # Dumper/MoFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Loader\MoFileLoader; use Symfony\Component\Translation\MessageCatalogue; /** * MoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class MoFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $sources = $targets = $sourceOffsets = $targetOffsets = ''; $offsets = []; $size = 0; foreach ($messages->all($domain) as $source => $target) { $offsets[] = array_map('strlen', [$sources, $source, $targets, $target]); $sources .= "\0".$source; $targets .= "\0".$target; ++$size; } $header = [ 'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC, 'formatRevision' => 0, 'count' => $size, 'offsetId' => MoFileLoader::MO_HEADER_SIZE, 'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size), 'sizeHashes' => 0, 'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size), ]; $sourcesSize = \strlen($sources); $sourcesStart = $header['offsetHashes'] + 1; foreach ($offsets as $offset) { $sourceOffsets .= $this->writeLong($offset[1]) .$this->writeLong($offset[0] + $sourcesStart); $targetOffsets .= $this->writeLong($offset[3]) .$this->writeLong($offset[2] + $sourcesStart + $sourcesSize); } $output = implode('', array_map([$this, 'writeLong'], $header)) .$sourceOffsets .$targetOffsets .$sources .$targets ; return $output; } /** * {@inheritdoc} */ protected function getExtension() { return 'mo'; } private function writeLong($str): string { return pack('V*', $str); } } PK!O3Dumper/XliffFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * XliffFileDumper generates xliff files from a message catalogue. * * @author Michel Salib */ class XliffFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $xliffVersion = '1.2'; if (\array_key_exists('xliff_version', $options)) { $xliffVersion = $options['xliff_version']; } if (\array_key_exists('default_locale', $options)) { $defaultLocale = $options['default_locale']; } else { $defaultLocale = \Locale::getDefault(); } if ('1.2' === $xliffVersion) { return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); } if ('2.0' === $xliffVersion) { return $this->dumpXliff2($defaultLocale, $messages, $domain); } throw new InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); } /** * {@inheritdoc} */ protected function getExtension() { return 'xlf'; } private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []) { $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; if (\array_key_exists('tool_info', $options)) { $toolInfo = array_merge($toolInfo, $options['tool_info']); } $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('version', '1.2'); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); $xliffFile = $xliff->appendChild($dom->createElement('file')); $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); $xliffFile->setAttribute('datatype', 'plaintext'); $xliffFile->setAttribute('original', 'file.ext'); $xliffHead = $xliffFile->appendChild($dom->createElement('header')); $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); foreach ($toolInfo as $id => $value) { $xliffTool->setAttribute($id, $value); } $xliffBody = $xliffFile->appendChild($dom->createElement('body')); foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('trans-unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); $translation->setAttribute('resname', $source); $s = $translation->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); $metadata = $messages->getMetadata($source, $domain); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $translation->appendChild($targetElement); $t->appendChild($text); if ($this->hasMetadataArrayInfo('notes', $metadata)) { foreach ($metadata['notes'] as $note) { if (!isset($note['content'])) { continue; } $n = $translation->appendChild($dom->createElement('note')); $n->appendChild($dom->createTextNode($note['content'])); if (isset($note['priority'])) { $n->setAttribute('priority', $note['priority']); } if (isset($note['from'])) { $n->setAttribute('from', $note['from']); } } } $xliffBody->appendChild($translation); } return $dom->saveXML(); } private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain) { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); $xliff->setAttribute('version', '2.0'); $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); $xliffFile = $xliff->appendChild($dom->createElement('file')); if (MessageCatalogue::INTL_DOMAIN_SUFFIX === substr($domain, -($suffixLength = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)))) { $xliffFile->setAttribute('id', substr($domain, 0, -$suffixLength).'.'.$messages->getLocale()); } else { $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); } foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); if (\strlen($source) <= 80) { $translation->setAttribute('name', $source); } $metadata = $messages->getMetadata($source, $domain); // Add notes section if ($this->hasMetadataArrayInfo('notes', $metadata)) { $notesElement = $dom->createElement('notes'); foreach ($metadata['notes'] as $note) { $n = $dom->createElement('note'); $n->appendChild($dom->createTextNode($note['content'] ?? '')); unset($note['content']); foreach ($note as $name => $value) { $n->setAttribute($name, $value); } $notesElement->appendChild($n); } $translation->appendChild($notesElement); } $segment = $translation->appendChild($dom->createElement('segment')); $s = $segment->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $segment->appendChild($targetElement); $t->appendChild($text); $xliffFile->appendChild($translation); } return $dom->saveXML(); } private function hasMetadataArrayInfo(string $key, array $metadata = null): bool { return null !== $metadata && \array_key_exists($key, $metadata) && ($metadata[$key] instanceof \Traversable || \is_array($metadata[$key])); } } PK!RRDumper/PhpFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PhpFileDumper generates PHP files from a message catalogue. * * @author Michel Salib */ class PhpFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { return "all($domain), true).";\n"; } /** * {@inheritdoc} */ protected function getExtension() { return 'php'; } } PK!SH44Dumper/YamlFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\ArrayConverter; use Symfony\Component\Yaml\Yaml; /** * YamlFileDumper generates yaml files from a message catalogue. * * @author Michel Salib */ class YamlFileDumper extends FileDumper { private $extension; public function __construct(string $extension = 'yml') { $this->extension = $extension; } /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { if (!class_exists(Yaml::class)) { throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); } $data = $messages->all($domain); if (isset($options['as_tree']) && $options['as_tree']) { $data = ArrayConverter::expandToTree($data); } if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) { return Yaml::dump($data, $inline); } return Yaml::dump($data); } /** * {@inheritdoc} */ protected function getExtension() { return $this->extension; } } PK!Dumper/PoFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class PoFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = 'msgid ""'."\n"; $output .= 'msgstr ""'."\n"; $output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n"; $output .= '"Content-Transfer-Encoding: 8bit\n"'."\n"; $output .= '"Language: '.$messages->getLocale().'\n"'."\n"; $output .= "\n"; $newLine = false; foreach ($messages->all($domain) as $source => $target) { if ($newLine) { $output .= "\n"; } else { $newLine = true; } $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['comments'])) { $output .= $this->formatComments($metadata['comments']); } if (isset($metadata['flags'])) { $output .= $this->formatComments(implode(',', (array) $metadata['flags']), ','); } if (isset($metadata['sources'])) { $output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':'); } $sourceRules = $this->getStandardRules($source); $targetRules = $this->getStandardRules($target); if (2 == \count($sourceRules) && [] !== $targetRules) { $output .= sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0])); $output .= sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1])); foreach ($targetRules as $i => $targetRule) { $output .= sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule)); } } else { $output .= sprintf('msgid "%s"'."\n", $this->escape($source)); $output .= sprintf('msgstr "%s"'."\n", $this->escape($target)); } } return $output; } private function getStandardRules(string $id) { // Partly copied from TranslatorTrait::trans. $parts = []; if (preg_match('/^\|++$/', $id)) { $parts = explode('|', $id); } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { $parts = $matches[0]; } $intervalRegexp = <<<'EOF' /^(?P ({\s* (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) \s*}) | (?P[\[\]]) \s* (?P-Inf|\-?\d+(\.\d+)?) \s*,\s* (?P\+?Inf|\-?\d+(\.\d+)?) \s* (?P[\[\]]) )\s*(?P.*?)$/xs EOF; $standardRules = []; foreach ($parts as $part) { $part = trim(str_replace('||', '|', $part)); if (preg_match($intervalRegexp, $part)) { // Explicit rule is not a standard rule. return []; } else { $standardRules[] = $part; } } return $standardRules; } /** * {@inheritdoc} */ protected function getExtension() { return 'po'; } private function escape(string $str): string { return addcslashes($str, "\0..\37\42\134"); } private function formatComments($comments, string $prefix = ''): ?string { $output = null; foreach ((array) $comments as $comment) { $output .= sprintf('#%s %s'."\n", $prefix, $comment); } return $output; } } PK!+_hhDumper/FileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s). * * Options: * - path (mandatory): the directory where the files should be saved * * @author Michel Salib */ abstract class FileDumper implements DumperInterface { /** * A template for the relative paths to files. * * @var string */ protected $relativePathTemplate = '%domain%.%locale%.%extension%'; /** * Sets the template for the relative paths to files. * * @param string $relativePathTemplate A template for the relative paths to files */ public function setRelativePathTemplate(string $relativePathTemplate) { $this->relativePathTemplate = $relativePathTemplate; } /** * {@inheritdoc} */ public function dump(MessageCatalogue $messages, array $options = []) { if (!\array_key_exists('path', $options)) { throw new InvalidArgumentException('The file dumper needs a path option.'); } // save a file for each domain foreach ($messages->getDomains() as $domain) { $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); if (!file_exists($fullpath)) { $directory = \dirname($fullpath); if (!file_exists($directory) && !@mkdir($directory, 0777, true)) { throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory)); } } $intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; $intlMessages = $messages->all($intlDomain); if ($intlMessages) { $intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale()); file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options)); $messages->replace([], $intlDomain); try { if ($messages->all($domain)) { file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } continue; } finally { $messages->replace($intlMessages, $intlDomain); } } file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } } /** * Transforms a domain of a message catalogue to its string representation. * * @return string representation */ abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []); /** * Gets the file extension of the dumper. * * @return string file extension */ abstract protected function getExtension(); /** * Gets the relative file path using the template. */ private function getRelativePath(string $domain, string $locale): string { return strtr($this->relativePathTemplate, [ '%domain%' => $domain, '%locale%' => $locale, '%extension%' => $this->getExtension(), ]); } } PK!8r~wDumper/JsonFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * JsonFileDumper generates an json formatted string representation of a message catalogue. * * @author singles */ class JsonFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; return json_encode($messages->all($domain), $flags); } /** * {@inheritdoc} */ protected function getExtension() { return 'json'; } } PK!7Dumper/DumperInterface.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * DumperInterface is the interface implemented by all translation dumpers. * There is no common option. * * @author Michel Salib */ interface DumperInterface { /** * Dumps the message catalogue. * * @param array $options Options that are used by the dumper */ public function dump(MessageCatalogue $messages, array $options = []); } PK!;ƖDumper/QtFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * QtFileDumper generates ts files from a message catalogue. * * @author Benjamin Eberlei */ class QtFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $ts = $dom->appendChild($dom->createElement('TS')); $context = $ts->appendChild($dom->createElement('context')); $context->appendChild($dom->createElement('name', $domain)); foreach ($messages->all($domain) as $source => $target) { $message = $context->appendChild($dom->createElement('message')); $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['sources'])) { foreach ((array) $metadata['sources'] as $location) { $loc = explode(':', $location, 2); $location = $message->appendChild($dom->createElement('location')); $location->setAttribute('filename', $loc[0]); if (isset($loc[1])) { $location->setAttribute('line', $loc[1]); } } } $message->appendChild($dom->createElement('source', $source)); $message->appendChild($dom->createElement('translation', $target)); } return $dom->saveXML(); } /** * {@inheritdoc} */ protected function getExtension() { return 'ts'; } } PK!LDumper/IniFileDumper.phpnuIw * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IniFileDumper generates an ini formatted string representation of a message catalogue. * * @author Stealth35 */ class IniFileDumper extends FileDumper { /** * {@inheritdoc} */ public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = ''; foreach ($messages->all($domain) as $source => $target) { $escapeTarget = str_replace('"', '\"', $target); $output .= $source.'="'.$escapeTarget."\"\n"; } return $output; } /** * {@inheritdoc} */ protected function getExtension() { return 'ini'; } } PK!™parser_current.rbnu[PK!XJDDruby_parser.rbnu[PK! jC C \ripper/sexp.rbnu[PK!,>0  ripper/lexer.rbnu[PK!!ۘ&ripper/shim.rbnu[PK!n~!!-parser_versions.rbnu[PK!hparser/lexer.rbnu[PK!׫zwparser/builder.rbnu[PK! O0s's'parser/compiler.rbnu[PK!+FU%% ripper.rbnu[PK!Gݩ88 (parser.rbnu[PK!,8%`Writer/TranslationWriterInterface.phpnuIwPK!8"NNeWriter/TranslationWriter.phpnuIwPK!c|:%mReader/TranslationReaderInterface.phpnuIwPK!ȳ}pReader/TranslationReader.phpnuIwPK!< b`` wCHANGELOG.mdnuIwPK!_ 9README.mdnuIwPK!ꪴ\\"Extractor/PhpStringTokenParser.phpnuIwPK!qdD#ˤExtractor/AbstractFileExtractor.phpnuIwPK!##Extractor/PhpExtractor.phpnuIwPK!/* Extractor/ExtractorInterface.phpnuIwPK!xp:CCExtractor/ChainExtractor.phpnuIwPK! ))LICENSEnuIwPK!M  Loader/QtFileLoader.phpnuIwPK!.!QLoader/PhpFileLoader.phpnuIwPK!00%Loader/IniFileLoader.phpnuIwPK!ddLoader/LoaderInterface.phpnuIwPK!=' ' KLoader/IcuResFileLoader.phpnuIwPK!zLoader/FileLoader.phpnuIwPK!jSLoader/ArrayLoader.phpnuIwPK!u'Loader/PoFileLoader.phpnuIwPK!'#Loader/JsonFileLoader.phpnuIwPK!|cc)Loader/YamlFileLoader.phpnuIwPK!DoE0Loader/CsvFileLoader.phpnuIwPK!L87Loader/XliffFileLoader.phpnuIwPK!w/ҫ44VLoader/IcuDatFileLoader.phpnuIwPK!vH@^Loader/MoFileLoader.phpnuIwPK!ȲppHoDataCollectorTranslator.phpnuIwPK!?S%S%Command/XliffLintCommand.phpnuIwPK!MessageCatalogueInterface.phpnuIwPK!Gv>>LoggingTranslator.phpnuIwPK!8&<77DTranslator.phpnuIwPK!δ Rcomposer.jsonnuIwPK!|E 0 0 CUtil/ArrayConverter.phpnuIwPK!Util/XliffUtils.phpnuIwPK!9HH)TranslatableMessage.phpnuIwPK!R99O/MetadataAwareInterface.phpnuIwPK!N &5DependencyInjection/TranslatorPass.phpnuIwPK!eʱ0CDependencyInjection/TranslationExtractorPass.phpnuIwPK!+JDependencyInjection/TranslatorPathsPass.phpnuIwPK!I*OO-`DependencyInjection/TranslationDumperPass.phpnuIwPK!ϲ$eFormatter/IntlFormatterInterface.phpnuIwPK!hFormatter/IntlFormatter.phpnuIwPK!!{{qFormatter/MessageFormatter.phpnuIwPK!h kk'hxFormatter/MessageFormatterInterface.phpnuIwPK! 4pp*|PluralizationRules.phpnuIwPK!C  Catalogue/OperationInterface.phpnuIwPK!jNYYڝCatalogue/MergeOperation.phpnuIwPK!Iqn n Catalogue/TargetOperation.phpnuIwPK!_:U&:Catalogue/AbstractOperation.phpnuIwPK!=^22+Resources/functions.phpnuIwPK!$Resources/bin/translation-status.phpnuIwPK!; ; Resources/data/parents.jsonnuIwPK!a+FResources/schemas/xliff-core-1.2-strict.xsdnuIwPK!DlAlA$Resources/schemas/xliff-core-2.0.xsdnuIwPK!"@""bResources/schemas/xml.xsdnuIwPK!?[ ;Exception/ExceptionInterface.phpnuIwPK! &`Exception/InvalidResourceException.phpnuIwPK![NException/RuntimeException.phpnuIwPK!GP'Exception/NotFoundResourceException.phpnuIwPK!Ca%Exception/LogicException.phpnuIwPK!>O  &XException/InvalidArgumentException.phpnuIwPK!.!iH#H#MessageCatalogue.phpnuIwPK!pF*C DataCollector/TranslationDataCollector.phpnuIwPK![Q3j' IdentityTranslator.phpnuIwPK!B))E* TranslatorBagInterface.phpnuIwPK!** - PseudoLocalizationTranslator.phpnuIwPK!vu X Dumper/IcuResFileDumper.phpnuIwPK!o`R~~d Dumper/CsvFileDumper.phpnuIwPK!0H# # j Dumper/MoFileDumper.phpnuIwPK!O3s Dumper/XliffFileDumper.phpnuIwPK!RR Dumper/PhpFileDumper.phpnuIwPK!SH44 Dumper/YamlFileDumper.phpnuIwPK! Dumper/PoFileDumper.phpnuIwPK!+_hh Dumper/FileDumper.phpnuIwPK!8r~w Dumper/JsonFileDumper.phpnuIwPK!7 Dumper/DumperInterface.phpnuIwPK!;Ɩ Dumper/QtFileDumper.phpnuIwPK!L Dumper/IniFileDumper.phpnuIwPKYYH