PageRenderTime 50ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/rubocop/result_cache.rb

https://gitlab.com/bglusman/rubocop
Ruby | 123 lines | 81 code | 14 blank | 28 comment | 8 complexity | 9eb69c4448e9c3aa110f007efa30e664 MD5 | raw file
  1. # encoding: utf-8
  2. require 'digest/md5'
  3. require 'find'
  4. require 'tmpdir'
  5. require 'etc'
  6. module RuboCop
  7. # Provides functionality for caching rubocop runs.
  8. class ResultCache
  9. # Include the user name in the path as a simple means of avoiding write
  10. # collisions.
  11. def initialize(file, options, config_store, cache_root = nil)
  12. cache_root ||= ResultCache.cache_root(config_store)
  13. @path = File.join(cache_root, rubocop_checksum, RUBY_VERSION,
  14. relevant_options(options),
  15. file_checksum(file, config_store))
  16. end
  17. def valid?
  18. File.exist?(@path)
  19. end
  20. def load
  21. Marshal.load(IO.binread(@path))
  22. end
  23. def save(offenses, disabled_line_ranges, comments)
  24. FileUtils.mkdir_p(File.dirname(@path))
  25. preliminary_path = "#{@path}_#{rand(1_000_000_000)}"
  26. File.open(preliminary_path, 'wb') do |f|
  27. # The Hash[x.sort] call is a trick that converts a Hash with a default
  28. # block to a Hash without a default block. Thus making it possible to
  29. # dump.
  30. f.write(Marshal.dump([offenses, Hash[disabled_line_ranges.sort],
  31. comments]))
  32. end
  33. # The preliminary path is used so that if there are multiple RuboCop
  34. # processes trying to save data for the same inspected file
  35. # simultaneously, the only problem we run in to is a competition who gets
  36. # to write to the final file. The contents are the same, so no corruption
  37. # of data should occur.
  38. FileUtils.mv(preliminary_path, @path)
  39. end
  40. # Remove old files so that the cache doesn't grow too big. When the
  41. # threshold MaxFilesInCache has been exceeded, the oldest 50% all the files
  42. # in the cache are removed. The reason for removing so much is that
  43. # cleaning should be done relatively seldom, since there is a slight risk
  44. # that some other RuboCop process was just about to read the file, when
  45. # there's parallel execution and the cache is shared.
  46. def self.cleanup(config_store, verbose, cache_root = nil)
  47. return if inhibit_cleanup # OPTIMIZE: For faster testing
  48. cache_root ||= cache_root(config_store)
  49. return unless File.exist?(cache_root)
  50. files, dirs = Find.find(cache_root).partition { |path| File.file?(path) }
  51. if files.length > config_store.for('.')['AllCops']['MaxFilesInCache'] &&
  52. files.length > 1
  53. # Add 1 to half the number of files, so that we remove the file if
  54. # there's only 1 left.
  55. remove_count = 1 + files.length / 2
  56. if verbose
  57. puts "Removing the #{remove_count} oldest files from #{cache_root}"
  58. end
  59. sorted = files.sort_by { |path| File.mtime(path) }
  60. begin
  61. sorted[0, remove_count].each { |path| File.delete(path) }
  62. dirs.each { |dir| Dir.rmdir(dir) if Dir["#{dir}/*"].empty? }
  63. rescue Errno::ENOENT
  64. # This can happen if parallel RuboCop invocations try to remove the
  65. # same files. No problem.
  66. puts $ERROR_INFO if verbose
  67. end
  68. end
  69. end
  70. private
  71. def self.cache_root(config_store)
  72. root = config_store.for('.')['AllCops']['CacheRootDirectory']
  73. root = File.join(Dir.tmpdir, Etc.getlogin) if root == '/tmp'
  74. File.join(root, 'rubocop_cache')
  75. end
  76. def file_checksum(file, config_store)
  77. Digest::MD5.hexdigest(Dir.pwd + file + IO.read(file) +
  78. config_store.for(file).to_s)
  79. rescue Errno::ENOENT
  80. # Spurious files that come and go should not cause a crash, at least not
  81. # here.
  82. '_'
  83. end
  84. class << self
  85. attr_accessor :source_checksum, :inhibit_cleanup
  86. end
  87. # The checksum of the rubocop program running the inspection.
  88. def rubocop_checksum
  89. ResultCache.source_checksum ||=
  90. begin
  91. lib_root = File.join(File.dirname(__FILE__), '..')
  92. bin_root = File.join(lib_root, '..', 'bin')
  93. source = Find.find(lib_root, bin_root).sort.map do |path|
  94. IO.read(path) if File.file?(path)
  95. end
  96. Digest::MD5.hexdigest(source.join)
  97. end
  98. end
  99. NON_CHANGING = [:color, :format, :formatters, :out, :debug, :fail_level,
  100. :cache, :fail_fast, :stdin]
  101. # Return the options given at invocation, minus the ones that have no
  102. # effect on which offenses and disabled line ranges are found, and thus
  103. # don't affect caching.
  104. def relevant_options(options)
  105. options = options.reject { |key, _| NON_CHANGING.include?(key) }
  106. options.to_s.gsub(/[^a-z]+/i, '_')
  107. end
  108. end
  109. end