# frozen_string_literal: truerequire'digest/md5'require'find'require'etc'moduleRuboCop# Provides functionality for caching rubocop runs.classResultCacheNON_CHANGING=%i[color format formatters out debug fail_level
cache fail_fast stdin parallel].freeze# Remove old files so that the cache doesn't grow too big. When the# threshold MaxFilesInCache has been exceeded, the oldest 50% of all the# files in the cache are removed. The reason for removing so much is that# cleaning should be done relatively seldom, since there is a slight risk# that some other RuboCop process was just about to read the file, when# there's parallel execution and the cache is shared.defself.cleanup(config_store,verbose,cache_root=nil)returnifinhibit_cleanup# OPTIMIZE: For faster testingcache_root||=cache_root(config_store)returnunlessFile.exist?(cache_root)files,dirs=Find.find(cache_root).partition{|path|File.file?(path)}returnunlessrequires_file_removal?(files.length,config_store)remove_oldest_files(files,dirs,cache_root,verbose)endclass<<selfprivatedefrequires_file_removal?(file_count,config_store)file_count>1&&file_count>config_store.for('.').for_all_cops['MaxFilesInCache']enddefremove_oldest_files(files,dirs,cache_root,verbose)# Add 1 to half the number of files, so that we remove the file if# there's only 1 left.remove_count=1+files.length/2ifverboseputs"Removing the #{remove_count} oldest files from #{cache_root}"endsorted=files.sort_by{|path|File.mtime(path)}remove_files(sorted,dirs,remove_count)rescueErrno::ENOENT# This can happen if parallel RuboCop invocations try to remove the# same files. No problem.puts$ERROR_INFOifverboseenddefremove_files(files,dirs,remove_count)# Batch file deletions, deleting over 130,000+ files will crash# File.delete.files[0,remove_count].each_slice(10_000).eachdo|files_slice|File.delete(*files_slice)enddirs.each{|dir|Dir.rmdir(dir)ifDir["#{dir}/*"].empty?}endenddefself.cache_root(config_store)root=config_store.for('.').for_all_cops['CacheRootDirectory']root||=ifENV.key?('XDG_CACHE_HOME')# Include user ID in the path to make sure the user has write# access.File.join(ENV['XDG_CACHE_HOME'],Process.uid.to_s)elseFile.join(ENV['HOME'],'.cache')endFile.join(root,'rubocop_cache')enddefself.allow_symlinks_in_cache_location?(config_store)config_store.for('.').for_all_cops['AllowSymlinksInCacheRootDirectory']enddefinitialize(file,options,config_store,cache_root=nil)cache_root||=ResultCache.cache_root(config_store)@allow_symlinks_in_cache_location=ResultCache.allow_symlinks_in_cache_location?(config_store)@path=File.join(cache_root,rubocop_checksum,relevant_options_digest(options),file_checksum(file,config_store))@cached_data=CachedData.new(file)enddefvalid?File.exist?(@path)enddefload@cached_data.from_json(IO.read(@path,encoding: Encoding::UTF_8))enddefsave(offenses)dir=File.dirname(@path)FileUtils.mkdir_p(dir)preliminary_path="#{@path}_#{rand(1_000_000_000)}"# RuboCop must be in control of where its cached data is stored. A# symbolic link anywhere in the cache directory tree can be an# indication that a symlink attack is being waged.returnifsymlink_protection_triggered?(dir)File.open(preliminary_path,'w',encoding: Encoding::UTF_8)do|f|f.write(@cached_data.to_json(offenses))end# The preliminary path is used so that if there are multiple RuboCop# processes trying to save data for the same inspected file# simultaneously, the only problem we run in to is a competition who gets# to write to the final file. The contents are the same, so no corruption# of data should occur.FileUtils.mv(preliminary_path,@path)endprivatedefsymlink_protection_triggered?(path)!@allow_symlinks_in_cache_location&&any_symlink?(path)enddefany_symlink?(path)whilepath!=File.dirname(path)ifFile.symlink?(path)warn"Warning: #{path} is a symlink, which is not allowed."returntrueendpath=File.dirname(path)endfalseenddeffile_checksum(file,config_store)Digest::MD5.hexdigest(Dir.pwd+file+IO.binread(file)+File.stat(file).mode.to_s+config_store.for(file).to_s)rescueErrno::ENOENT# Spurious files that come and go should not cause a crash, at least not# here.'_'endclass<<selfattr_accessor:source_checksum,:inhibit_cleanupend# The checksum of the rubocop program running the inspection.defrubocop_checksumResultCache.source_checksum||=beginlib_root=File.join(File.dirname(__FILE__),'..')bin_root=File.join(lib_root,'..','bin')# These are all the files we have `require`d plus everything in the# bin directory. A change to any of them could affect the cop output# so we include them in the cache hash.source_files=$LOADED_FEATURES+Find.find(bin_root).to_asources=source_files.select{|path|File.file?(path)}.sort.map{|path|IO.read(path,encoding: Encoding::UTF_8)}Digest::MD5.hexdigest(sources.join)endend# Return a hash of the options given at invocation, minus the ones that have# no effect on which offenses and disabled line ranges are found, and thus# don't affect caching.defrelevant_options_digest(options)options=options.reject{|key,_|NON_CHANGING.include?(key)}options=options.to_s.gsub(/[^a-z]+/i,'_')# We must avoid making file names too long for some filesystems to handle# If they are short, we can leave them human-readableoptions.length<=32?options:Digest::MD5.hexdigest(options)endendend