rubocop-hq/rubocop

View on GitHub
lib/rubocop/server/cache.rb

Summary

Maintainability
A
1 hr
Test Coverage
B
86%
# frozen_string_literal: true

require 'pathname'
require_relative '../cache_config'
require_relative '../config_finder'

#
# This code is based on https://github.com/fohte/rubocop-daemon.
#
# Copyright (c) 2018 Hayato Kawai
#
# The MIT License (MIT)
#
# https://github.com/fohte/rubocop-daemon/blob/master/LICENSE.txt
#
module RuboCop
  module Server
    # Caches the states of server process.
    # @api private
    class Cache
      GEMFILE_NAMES = %w[Gemfile gems.rb].freeze

      class << self
        attr_accessor :cache_root_path

        # Searches for Gemfile or gems.rb in the current dir or any parent dirs
        def project_dir
          current_dir = Dir.pwd
          while current_dir != '/'
            return current_dir if GEMFILE_NAMES.any? do |gemfile|
              File.exist?(File.join(current_dir, gemfile))
            end

            current_dir = File.expand_path('..', current_dir)
          end
          # If we can't find a Gemfile, just use the current directory
          Dir.pwd
        end

        def project_dir_cache_key
          @project_dir_cache_key ||= project_dir[1..].tr('/', '+')
        end

        def dir
          Pathname.new(File.join(cache_path, project_dir_cache_key)).tap do |d|
            d.mkpath unless d.exist?
          end
        end

        def cache_path
          cache_root_dir = if cache_root_path
                             File.join(cache_root_path, 'rubocop_cache')
                           else
                             cache_root_dir_from_config
                           end

          File.expand_path(File.join(cache_root_dir, 'server'))
        end

        # rubocop:disable Metrics/MethodLength
        def cache_root_dir_from_config
          CacheConfig.root_dir do
            # `RuboCop::ConfigStore` has heavy dependencies, this is a lightweight implementation
            # so that only the necessary `CacheRootDirectory` can be obtained.
            config_path = ConfigFinder.find_config_path(Dir.pwd)
            file_contents = File.read(config_path)

            # Returns early if `CacheRootDirectory` is not used before requiring `erb` or `yaml`.
            next unless file_contents.include?('CacheRootDirectory')

            require 'erb'
            yaml_code = ERB.new(file_contents).result

            require 'yaml'
            config_yaml = YAML.safe_load(
              yaml_code, permitted_classes: [Regexp, Symbol], aliases: true
            )

            # For compatibility with Ruby 3.0 or lower.
            if Gem::Version.new(Psych::VERSION) < Gem::Version.new('4.0.0')
              config_yaml == false ? nil : config_yaml
            end

            config_yaml&.dig('AllCops', 'CacheRootDirectory')
          end
        end
        # rubocop:enable Metrics/MethodLength

        def port_path
          dir.join('port')
        end

        def token_path
          dir.join('token')
        end

        def pid_path
          dir.join('pid')
        end

        def lock_path
          dir.join('lock')
        end

        def status_path
          dir.join('status')
        end

        def version_path
          dir.join('version')
        end

        def stderr_path
          dir.join('stderr')
        end

        def pid_running?
          Process.kill(0, pid_path.read.to_i) == 1
        rescue Errno::ESRCH, Errno::ENOENT, Errno::EACCES, Errno::EROFS
          false
        end

        def acquire_lock
          lock_file = File.open(lock_path, File::CREAT)
          # flock returns 0 if successful, and false if not.
          flock_result = lock_file.flock(File::LOCK_EX | File::LOCK_NB)
          yield flock_result != false
        ensure
          lock_file.flock(File::LOCK_UN)
          lock_file.close
        end

        def write_port_and_token_files(port:, token:)
          port_path.write(port)
          token_path.write(token)
        end

        def write_pid_file
          pid_path.write(Process.pid)
          yield
        ensure
          dir.rmtree
        end

        def write_status_file(status)
          status_path.write(status)
        end

        def write_version_file(version)
          version_path.write(version)
        end
      end
    end
  end
end