# frozen_string_literal: true module RuboCop module Cop module Rails # Use `Rails.root` IO methods instead of passing it to `File`. # # `Rails.root` is an instance of `Pathname` # so we can apply many IO methods directly. # # This cop works best when used together with # `Style/FileRead`, `Style/FileWrite` and `Rails/RootJoinChain`. # # @safety # This cop is unsafe for autocorrection because ``Dir``'s `children`, `each_child`, `entries`, and `glob` # methods return string element, but these methods of `Pathname` return `Pathname` element. # # @example # # bad # File.open(Rails.root.join('db', 'schema.rb')) # File.open(Rails.root.join('db', 'schema.rb'), 'w') # File.read(Rails.root.join('db', 'schema.rb')) # File.binread(Rails.root.join('db', 'schema.rb')) # File.write(Rails.root.join('db', 'schema.rb'), content) # File.binwrite(Rails.root.join('db', 'schema.rb'), content) # Dir.glob(Rails.root.join('db', 'schema.rb')) # Dir[Rails.root.join('db', 'schema.rb')] # # # good # Rails.root.join('db', 'schema.rb').open # Rails.root.join('db', 'schema.rb').open('w') # Rails.root.join('db', 'schema.rb').read # Rails.root.join('db', 'schema.rb').binread # Rails.root.join('db', 'schema.rb').write(content) # Rails.root.join('db', 'schema.rb').binwrite(content) # Rails.root.glob("db/schema.rb") # class RootPathnameMethods < Base # rubocop:disable Metrics/ClassLength extend AutoCorrector include RangeHelp MSG = '`%s` is a `Pathname`, so you can use `%s`.' DIR_GLOB_METHODS = %i[[] glob].to_set.freeze DIR_NON_GLOB_METHODS = %i[ children delete each_child empty? entries exist? mkdir open rmdir unlink ].to_set.freeze DIR_METHODS = (DIR_GLOB_METHODS + DIR_NON_GLOB_METHODS).freeze FILE_METHODS = %i[ atime basename binread binwrite birthtime blockdev? chardev? chmod chown ctime delete directory? dirname empty? executable? executable_real? exist? expand_path extname file? fnmatch fnmatch? ftype grpowned? join lchmod lchown lstat mtime open owned? pipe? read readable? readable_real? readlines readlink realdirpath realpath rename setgid? setuid? size size? socket? split stat sticky? symlink? sysopen truncate unlink utime world_readable? world_writable? writable? writable_real? write zero? ].to_set.freeze FILE_TEST_METHODS = %i[ blockdev? chardev? directory? empty? executable? executable_real? exist? file? grpowned? owned? pipe? readable? readable_real? setgid? setuid? size size? socket? sticky? symlink? world_readable? world_writable? writable? writable_real? zero? ].to_set.freeze FILE_UTILS_METHODS = %i[chmod chown mkdir mkpath rmdir rmtree].to_set.freeze RESTRICT_ON_SEND = (DIR_METHODS + FILE_METHODS + FILE_TEST_METHODS + FILE_UTILS_METHODS).to_set.freeze # @!method pathname_method_for_ruby_2_5_or_higher(node) def_node_matcher :pathname_method_for_ruby_2_5_or_higher, <<~PATTERN { (send (const {nil? cbase} :Dir) $DIR_METHODS $_ $...) (send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...) (send (const {nil? cbase} :FileTest) $FILE_TEST_METHODS $_ $...) (send (const {nil? cbase} :FileUtils) $FILE_UTILS_METHODS $_ $...) } PATTERN # @!method pathname_method_for_ruby_2_4_or_lower(node) def_node_matcher :pathname_method_for_ruby_2_4_or_lower, <<~PATTERN { (send (const {nil? cbase} :Dir) $DIR_NON_GLOB_METHODS $_ $...) (send (const {nil? cbase} {:IO :File}) $FILE_METHODS $_ $...) (send (const {nil? cbase} :FileTest) $FILE_TEST_METHODS $_ $...) (send (const {nil? cbase} :FileUtils) $FILE_UTILS_METHODS $_ $...) } PATTERN def_node_matcher :dir_glob?, <<~PATTERN (send (const {cbase nil?} :Dir) DIR_GLOB_METHODS ...) PATTERN def_node_matcher :rails_root_pathname?, <<~PATTERN { $#rails_root? (send $#rails_root? :join ...) } PATTERN # @!method rails_root?(node) def_node_matcher :rails_root?, <<~PATTERN (send (const {nil? cbase} :Rails) {:root :public_path}) PATTERN def on_send(node) evidence(node) do |method, path, args, rails_root| replacement = if dir_glob?(node) build_path_glob_replacement(path) else build_path_replacement(path, method, args) end message = format(MSG, rails_root: rails_root.source, replacement: replacement) add_offense(node, message: message) do |corrector| corrector.replace(node, replacement) end end end private def evidence(node) return if node.method?(:open) && node.parent&.send_type? return unless (method, path, args = pathname_method(node)) && (rails_root = rails_root_pathname?(path)) yield(method, path, args, rails_root) end def pathname_method(node) if target_ruby_version >= 2.5 pathname_method_for_ruby_2_5_or_higher(node) else pathname_method_for_ruby_2_4_or_lower(node) end end def build_path_glob_replacement(path) receiver = range_between(path.source_range.begin_pos, path.children.first.loc.selector.end_pos).source argument = path.arguments.one? ? path.first_argument.source : join_arguments(path.arguments) "#{receiver}.glob(#{argument})" end def build_path_replacement(path, method, args) path_replacement = path.source if path.arguments? && !path.parenthesized_call? path_replacement[' '] = '(' path_replacement << ')' end replacement = "#{path_replacement}.#{method}" replacement += "(#{args.map(&:source).join(', ')})" unless args.empty? replacement end def include_interpolation?(arguments) arguments.any? do |argument| argument.children.any? { |child| child.respond_to?(:begin_type?) && child.begin_type? } end end def join_arguments(arguments) use_interpolation = false joined_arguments = arguments.map do |arg| if arg.respond_to?(:value) arg.value else use_interpolation = true "\#{#{arg.source}}" end end.join('/') quote = enforce_double_quotes? || include_interpolation?(arguments) || use_interpolation ? '"' : "'" "#{quote}#{joined_arguments}#{quote}" end def enforce_double_quotes? string_literals_config['EnforcedStyle'] == 'double_quotes' end def string_literals_config config.for_cop('Style/StringLiterals') end end end end end