Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rubocop/cop/lint/detect_invalid_pack_directives.rb
Views: 11784
# frozen_string_literal: true12module RuboCop3module Cop4module Lint5# Looks for invalid pack/unpack directives with Ruby 3.3.0 some directives6# that used to not raise errors, now will - context: https://bugs.ruby-lang.org/issues/19150:7# * Array#pack now raises ArgumentError for unknown directives8# * String#unpack now raises ArgumentError for unknown directives9#10# @example11# # bad12# ```13# 3.3.0-preview1 :003 > [0x1].pack('<L')14# <internal:pack>:8:in `pack': unknown pack directive '<' in '<L' (ArgumentError)15# ```16#17# # good18# ```19# 3.3.0-preview1 :001 > [0x1].pack('L<')20# => "\x01\x00\x00\x00"21# ```22class DetectInvalidPackDirectives < RuboCop::Cop::Base23# https://github.com/ruby/ruby/blob/7cfabe1acc55b24fc2c479a87efa71cf74e9e8fc/pack.c#L3824MODIFIABLE_DIRECTIVES = %w[s S i I l L q Q j J]2526# https://github.com/ruby/ruby/blob/7cfabe1acc55b24fc2c479a87efa71cf74e9e8fc/pack.c#L29827ACCEPTABLE_DIRECTIVES = %w[U m A B H a A Z b B h H c C s S i I l L q Q j J n N v V f F e E d D g G x X @ % U u m M P p w]2829# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method30# @return [[RuboCop::AST::Node], raise] offense when an invalid directive is found, or raise if unexpected error found31def on_send(node)32_callee, method_name = *node3334return unless %i[pack unpack pack1 unpack1].include?(method_name)3536args = node.arguments37return if args.empty?3839args.each do |arg|40next unless string_arg?(arg)4142# if multiline arguments are passed43if arg.type == :dstr44idx = []4546pack_directive = arg.children.map do |child|47if begin_arg?(child)48next49else50idx << child.children.first.length51child.children.join52end53end.join5455# elsif single line arguments are passed56elsif arg.type == :str57pack_directive = arg.children.first58end5960error = validate_directive(pack_directive)61if error.nil?62next63else64offense_range = get_error_range(arg, error[:index], idx)65return if offense_range.nil?6667add_offense(offense_range, message: error[:message])68end69end70end7172private7374# Check if the pack directives are valid. See link for pack docs https://apidock.com/ruby/Array/pack75#76# Code based on https://github.com/ruby/ruby/blob/6391132c03ac08da0483adb986ff9a54e41f9e14/pack.c#L19677# adapted into Ruby78#79# @param [String] pack_directive The ruby pack/unpack directive to validate80# @return [Hash,nil] A hash with the message and index that the invalidate directive was found at, or nil.81def validate_directive(pack_directive)82# current pointer value83p = 08485# end of pointer range86pend = pack_directive.length8788while p < pend89explicit_endian = 090type_index = p9192# get data type93type = pack_directive[type_index]94p += 19596if type.blank?97next98end99100if type == '#'101p += 1 while p < pend && pack_directive[p] != "\n"102next103end104105# Modifiers106loop do107case pack_directive[p]108when '_', '!'109if MODIFIABLE_DIRECTIVES.include?(type)110p += 1111else112return { message: "'#{pack_directive[p]}' allowed only after types #{MODIFIABLE_DIRECTIVES.join}", index: p }113end114when '<', '>'115unless MODIFIABLE_DIRECTIVES.include?(type)116return { message: "'#{pack_directive[p]}' allowed only after types #{MODIFIABLE_DIRECTIVES.join}", index: p }117end118119if explicit_endian != 0120return { message: "Can't use both '<' and '>'.", index: p }121end122123explicit_endian = pack_directive[p]124p += 1125else126break127end128end129130# Data length131if pack_directive[p] == '*'132p += 1133elsif pack_directive[p]&.match?(/\d/)134p += 1 while pack_directive[p]&.match?(/\d/)135end136137# Check type138unless ACCEPTABLE_DIRECTIVES.include?(type)139return { message: "unknown pack directive '#{type}' in '#{pack_directive}'", index: type_index }140end141end142143nil144end145146# Checks if the current node is of type `:str` or `:dstr` - `dstr` being multiline147#148# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method149# @return [TrueClass, FalseClass]150def string_arg?(node)151node&.type == :str || node&.type == :dstr152end153154# Check if the node if of type `:begin`155#156# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method157# @return [TrueClass, FalseClass]158def begin_arg?(node)159node.type == :begin160end161162# Get the range of the offense to more accurately raise offenses against specific directives163#164# @param [RuboCop::AST::DstrNode, RuboCop::AST::StrNode ] arg The node that need its range calculated165# @param [Integer] p The current pointer value166# @param [Array] idx An array holding to number of indexes for the node167# @return [Parser::Source::Range] The range of the node value168def get_error_range(arg, p, idx)169# Logic for multiline strings170if arg.type == :dstr171total = 0172index = 0173174idx.each_with_index do |idx_length, count|175if total < p176total += idx_length177index = count178end179end180adjusted_index = p - idx[0..(index - 1)].sum181182indexed_arg = arg.children[index]183184if begin_arg?(indexed_arg)185return nil186else187newline_adjustment = indexed_arg.children.first[0..adjusted_index].scan(/[\n\t]/).count188end189190# If there's opening quotes present, i.e. "a", instead of heredoc which doesn't have preceding opening quotes:191if indexed_arg.loc.begin192range_start = indexed_arg.loc.begin.end_pos + (p - adjusted_index)193else194expression = processed_source.raw_source[indexed_arg.loc.expression.begin.begin_pos...indexed_arg.loc.expression.end.end_pos]195if expression[/^\s+/].nil?196leading_whitespace_size = 0197else198leading_whitespace_size = expression[/^\s+/].length199end200adjusted_index += leading_whitespace_size201range_start = indexed_arg.loc.expression.begin_pos + (adjusted_index + newline_adjustment)202end203# Logic for single line strings204else205newline_adjustment = arg.children.first[0..p].scan(/[\n\t]/).count206range_start = arg.loc.begin.end_pos + (p + newline_adjustment)207end208209range_end = range_start + 1210211Parser::Source::Range.new(arg.loc.expression.source_buffer, range_start, range_end)212end213end214end215end216end217218219