Skip to content

Commit

Permalink
Merge pull request #268 from zachfeldman/zfeldman/cbruckmayer/impleme…
Browse files Browse the repository at this point in the history
…nt-file-cache

[Feature] Implement file cache
  • Loading branch information
etiennebarrie authored Oct 26, 2022
2 parents cc14978 + e082855 commit 3f14d38
Show file tree
Hide file tree
Showing 15 changed files with 499 additions and 25 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,23 @@ app/views/users/_graph.html.erb:27:37: Extra space detected where there should b
2 error(s) were found in ERB files
```

## Caching

The cache is currently opt-in - to turn it on, use the --cache option:

```sh
erblint --cache ./app
Cache mode is on
Linting 413 files with 15 linters...
File names pruned from the cache will be logged

No errors were found in ERB files
```

When the cache is on, lint results are stored in the `.erb-lint-cache` directory, in files with a filename computed with a hash of information about the file and `erb-lint` that should change when necessary. These files store instance attributes of the `CachedOffense` object, which only contain the `Offense` attributes necessary to restore the results of running `erb-lint` for output. The cache also automatically prunes outdated files each time it's run.

You can also use the --clear-cache option to delete the cache file directory.

## License

This project is released under the [MIT license](LICENSE.txt).
2 changes: 2 additions & 0 deletions lib/erb_lint/all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "rubocop"

require "erb_lint"
require "erb_lint/cache"
require "erb_lint/cached_offense"
require "erb_lint/corrector"
require "erb_lint/file_loader"
require "erb_lint/linter_config"
Expand Down
88 changes: 88 additions & 0 deletions lib/erb_lint/cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module ERBLint
class Cache
CACHE_DIRECTORY = ".erb-lint-cache"

def initialize(config, file_loader = nil)
@config = config
@file_loader = file_loader
@hits = []
@new_results = []
puts "Cache mode is on"
end

def get(filename, file_content)
file_checksum = checksum(filename, file_content)
begin
cache_file_contents_as_offenses = JSON.parse(
File.read(File.join(CACHE_DIRECTORY, file_checksum))
).map do |offense_hash|
ERBLint::CachedOffense.new(offense_hash)
end
rescue Errno::ENOENT
return false
end
@hits.push(file_checksum)
cache_file_contents_as_offenses
end

def set(filename, file_content, offenses_as_json)
file_checksum = checksum(filename, file_content)
@new_results.push(file_checksum)

FileUtils.mkdir_p(CACHE_DIRECTORY)

File.open(File.join(CACHE_DIRECTORY, file_checksum), "wb") do |f|
f.write(offenses_as_json)
end
end

def close
prune_cache
end

def prune_cache
if hits.empty?
puts "Cache being created for the first time, skipping prune"
return
end

cache_files = Dir.new(CACHE_DIRECTORY).children
cache_files.each do |cache_file|
next if hits.include?(cache_file) || new_results.include?(cache_file)

File.delete(File.join(CACHE_DIRECTORY, cache_file))
end
end

def cache_dir_exists?
File.directory?(CACHE_DIRECTORY)
end

def clear
return unless cache_dir_exists?

puts "Clearing cache by deleting cache directory"
FileUtils.rm_r(CACHE_DIRECTORY)
end

private

attr_reader :config, :hits, :new_results

def checksum(filename, file_content)
digester = Digest::SHA1.new
mode = File.stat(filename).mode

digester.update(
"#{mode}#{config.to_hash}#{ERBLint::VERSION}#{file_content}"
)
digester.hexdigest
rescue Errno::ENOENT
# Spurious files that come and go should not cause a crash, at least not
# here.
"_"
end
end
end
58 changes: 58 additions & 0 deletions lib/erb_lint/cached_offense.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module ERBLint
# A Cached version of an Offense with only essential information represented as strings
class CachedOffense
attr_reader(
:message,
:line_number,
:severity,
:column,
:simple_name,
:last_line,
:last_column,
:length,
)

def initialize(params)
params = params.transform_keys(&:to_sym)

@message = params[:message]
@line_number = params[:line_number]
@severity = params[:severity]&.to_sym
@column = params[:column]
@simple_name = params[:simple_name]
@last_line = params[:last_line]
@last_column = params[:last_column]
@length = params[:length]
end

def self.new_from_offense(offense)
new(
{
message: offense.message,
line_number: offense.line_number,
severity: offense.severity,
column: offense.column,
simple_name: offense.simple_name,
last_line: offense.last_line,
last_column: offense.last_column,
length: offense.length,
}
)
end

def to_h
{
message: message,
line_number: line_number,
severity: severity,
column: column,
simple_name: simple_name,
last_line: last_line,
last_column: last_column,
length: length,
}
end
end
end
69 changes: 64 additions & 5 deletions lib/erb_lint/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,26 @@ def initialize
def run(args = ARGV)
dupped_args = args.dup
load_options(dupped_args)

if cache? && autocorrect?
failure!("cannot run autocorrect mode with cache")
end

@files = @options[:stdin] || dupped_args

load_config

@cache = Cache.new(@config, file_loader) if cache? || clear_cache?

if clear_cache?
if cache.cache_dir_exists?
cache.clear
success!("cache directory cleared")
else
failure!("cache directory doesn't exist, skipping deletion.")
end
end

if !@files.empty? && lint_files.empty?
if allow_no_files?
success!("no files found...\n")
Expand Down Expand Up @@ -65,7 +81,7 @@ def run(args = ARGV)
lint_files.each do |filename|
runner.clear_offenses
begin
file_content = run_with_corrections(runner, filename)
file_content = run_on_file(runner, filename)
rescue => e
@stats.exceptions += 1
puts "Exception occurred when processing: #{relative_filename(filename)}"
Expand All @@ -77,6 +93,8 @@ def run(args = ARGV)
end
end

cache&.close

reporter.show

if stdin? && autocorrect?
Expand All @@ -99,13 +117,43 @@ def run(args = ARGV)

private

attr_reader :cache, :config

def run_on_file(runner, filename)
file_content = read_content(filename)

if cache? && !autocorrect?
run_using_cache(runner, filename, file_content)
else
file_content = run_with_corrections(runner, filename, file_content)
end

log_offense_stats(runner, filename)
file_content
end

def run_using_cache(runner, filename, file_content)
if (cache_result_offenses = cache.get(filename, file_content))
runner.restore_offenses(cache_result_offenses)
else
run_with_corrections(runner, filename, file_content)
cache.set(filename, file_content, runner.offenses.map(&:to_cached_offense_hash).to_json)
end
end

def autocorrect?
@options[:autocorrect]
end

def run_with_corrections(runner, filename)
file_content = read_content(filename)
def cache?
@options[:cache]
end

def clear_cache?
@options[:clear_cache]
end

def run_with_corrections(runner, filename, file_content)
7.times do
processed_source = ERBLint::ProcessedSource.new(filename, file_content)
runner.run(processed_source)
Expand All @@ -127,6 +175,11 @@ def run_with_corrections(runner, filename)
file_content = corrector.corrected_content
runner.clear_offenses
end

file_content
end

def log_offense_stats(runner, filename)
offenses_filename = relative_filename(filename)
offenses = runner.offenses || []

Expand All @@ -138,8 +191,6 @@ def run_with_corrections(runner, filename)

@stats.processed_files[offenses_filename] ||= []
@stats.processed_files[offenses_filename] |= offenses

file_content
end

def read_content(filename)
Expand Down Expand Up @@ -283,6 +334,14 @@ def option_parser
@options[:enabled_linters] = known_linter_names
end

opts.on("--cache", "Enable caching") do |config|
@options[:cache] = config
end

opts.on("--clear-cache", "Clear cache") do |config|
@options[:clear_cache] = config
end

opts.on("--enable-linters LINTER[,LINTER,...]", Array,
"Only use specified linter", "Known linters are: #{known_linter_names.join(", ")}") do |linters|
linters.each do |linter|
Expand Down
2 changes: 1 addition & 1 deletion lib/erb_lint/linter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def support_autocorrect?
end
end

attr_reader :offenses
attr_reader :offenses, :config

# Must be implemented by the concrete inheriting class.
def initialize(file_loader, config)
Expand Down
20 changes: 20 additions & 0 deletions lib/erb_lint/offense.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def initialize(linter, source_range, message, context = nil, severity = nil)
@severity = severity
end

def to_cached_offense_hash
ERBLint::CachedOffense.new_from_offense(self).to_h
end

def inspect
"#<#{self.class.name} linter=#{linter.class.name} "\
"source_range=#{source_range.begin_pos}...#{source_range.end_pos} "\
Expand All @@ -43,5 +47,21 @@ def line_number
def column
source_range.column
end

def simple_name
linter.class.simple_name
end

def last_line
source_range.last_line
end

def last_column
source_range.last_column
end

def length
source_range.length
end
end
end
8 changes: 4 additions & 4 deletions lib/erb_lint/reporters/json_reporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ def formatted_offenses(offenses)

def format_offense(offense)
{
linter: offense.linter.class.simple_name,
linter: offense.simple_name,
message: offense.message.to_s,
location: {
start_line: offense.line_number,
start_column: offense.column,
last_line: offense.source_range.last_line,
last_column: offense.source_range.last_column,
length: offense.source_range.length,
last_line: offense.last_line,
last_column: offense.last_column,
length: offense.length,
},
}
end
Expand Down
4 changes: 4 additions & 0 deletions lib/erb_lint/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ def clear_offenses
@offenses = []
@linters.each(&:clear_offenses)
end

def restore_offenses(offenses)
@offenses.concat(offenses)
end
end
end
Loading

0 comments on commit 3f14d38

Please sign in to comment.