Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Implement file cache #268

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fbf26a2
Implement file cache
ChrisBr Nov 4, 2020
17ce887
Initial cleaning implementation
zachfeldman Aug 12, 2022
b5aeb01
Rename clean to prune, implement clearing, add autocorrect error message
zachfeldman Aug 13, 2022
95b5e22
Lints and cops
zachfeldman Aug 13, 2022
7032a73
Add integration specs
zachfeldman Aug 13, 2022
6012ae8
Fix cache implementation, clean up code
zachfeldman Aug 14, 2022
ad47b98
Specs for cache and associated changes
zachfeldman Aug 15, 2022
fbbcd2a
Clean up
zachfeldman Aug 15, 2022
a292ef0
Include ERBLint version in cache key
zachfeldman Aug 19, 2022
be11370
Fix nonsensical error message
zachfeldman Aug 19, 2022
deeb4f8
Spacing fixes and removing unneeded code
zachfeldman Aug 19, 2022
1a5db05
Fix tests by mocking returned checksum
zachfeldman Aug 20, 2022
8d4eaa8
Move cache pruning to the cache class
zachfeldman Oct 8, 2022
cdf089c
Do less file reads and stats
zachfeldman Oct 8, 2022
b331799
Attempt a new implementation for storing offenses in a CachedOffense …
zachfeldman Oct 10, 2022
ce8d6da
Update lib/erb_lint/cached_offense.rb to avoid errors on nil severity
zachfeldman Oct 12, 2022
acba4a9
Support caching with json and compact reporters
eterry1388 Oct 12, 2022
6d492e4
Merge pull request #1 from eterry1388/eterry1388/zfeldman/cbruckmayer…
zachfeldman Oct 13, 2022
9ec34e3
Update lib/erb_lint/cli.rb to use safe navigation operator
zachfeldman Oct 13, 2022
877c28c
Rename with-cache to cache
zachfeldman Oct 13, 2022
7ea048c
Prune cache by default
zachfeldman Oct 13, 2022
a32816d
Add README for cache
zachfeldman Oct 13, 2022
e082855
Reduce output of pruning process
zachfeldman Oct 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/erb_lint/all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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
55 changes: 31 additions & 24 deletions lib/erb_lint/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,58 @@ module ERBLint
class Cache
CACHE_DIRECTORY = ".erb-lint-cache"

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this is handled in the rest of the library but maybe we should pass in an io or logger so we could silence these outputs?

Something like

def initialize(config, file_loader = nil, logger = STDOUT)
  # ...
  @logger = logger
  @logger.puts "Cache mode is on"
end

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a nice addition, the rest of the library though uses puts to output to $stdout though. I did try the code you recommended, but I'm not really sure if that allows an easy way to silence output (at least I couldn't Google it, and couldn't make it happen running the command without output unless I redirect to > /dev/null). Do you have a good example of this pattern in another lib? I could also go for a full blown logger instance with debug levels for most of the stuff in this file but then I feel like I'd want to apply that to all puts statements in the project.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the rest of the library uses puts this is fine.

To silence it in tests you can pass in a StringIO like

io = StringIO.new

Foo.new(io: io)

io.string

end

def get(filename, file_content)
JSON.parse(File.read(File.join(CACHE_DIRECTORY, checksum(filename)))).map do |offense|
ERBLint::Offense.from_json(offense, config, @file_loader, 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|
ERBLint::CachedOffense.from_json(offense)
end
rescue Errno::ENOENT
return false
end
@hits.push(file_checksum) if prune?
cache_file_contents_as_offenses
end

def include?(filename)
File.exist?(File.join(CACHE_DIRECTORY, checksum(filename)))
end
def set(filename, file_content, offenses_as_json)
file_checksum = checksum(filename, file_content)
@new_results.push(file_checksum) if prune?

def []=(filename, offenses_as_json)
FileUtils.mkdir_p(CACHE_DIRECTORY)

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

def add_hit(hit)
@hits.push(hit)
end

def add_new_result(filename)
@new_results.push(filename)
def close
prune_cache if prune?
end

def prune
def prune_cache
puts "Prune cache mode is on - pruned file names will be logged"
if hits.empty?
puts "Cache being created for the first time, skipping prune"
return
end

cache_files = Dir.new(CACHE_DIRECTORY).children
hits_as_checksums = hits.map { |hit| checksum(hit) }
new_results_as_checksums = new_results.map { |new_result| checksum(new_result) }
cache_files.each do |cache_file|
next if hits_as_checksums.include?(cache_file)
next if hits.include?(cache_file)

if new_results_as_checksums.include?(cache_file)
puts "Skipping deletion of new cache result #{cache_file} in prune"
if new_results.include?(cache_file)
puts "Skipping deletion of new cache result #{cache_file}"
next
end

Expand All @@ -77,19 +81,22 @@ def clear

attr_reader :config, :hits, :new_results

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

digester.update(
"#{file}#{mode}#{config.to_hash}#{ERBLint::VERSION}"
"#{mode}#{config.to_hash}#{ERBLint::VERSION}#{file_content}"
)
digester.file(file)
digester.hexdigest
rescue Errno::ENOENT
# Spurious files that come and go should not cause a crash, at least not
# here.
"_"
end

def prune?
@prune
end
end
end
39 changes: 39 additions & 0 deletions lib/erb_lint/cached_offense.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module ERBLint
# A Cached version of an Offense with only essential information represented as strings
class CachedOffense
attr_reader :line_number, :message, :severity

def initialize(message, line_number, severity)
@message = message
@line_number = line_number
@severity = severity
end

def self.new_from_offense(offense)
new(
offense.message,
offense.line_number.to_s,
offense.severity
)
end

def to_json_format
{
message: message,
line_number: line_number,
severity: severity,
}
end

def self.from_json(parsed_json)
parsed_json.transform_keys!(&:to_sym)
new(
parsed_json[:message],
parsed_json[:line_number],
parsed_json[:severity]&.to_sym
)
end
end
end
12 changes: 5 additions & 7 deletions lib/erb_lint/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def run(args = ARGV)

load_config

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

if clear_cache?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if clear_cache?
if clear_cache?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made my own commit to fix these newline suggestions!

if cache.cache_dir_exists?
Expand Down Expand Up @@ -93,7 +93,7 @@ def run(args = ARGV)
end
end

cache.prune if prune_cache?
cache.close if with_cache? || clear_cache?
zachfeldman marked this conversation as resolved.
Show resolved Hide resolved

reporter.show

Expand Down Expand Up @@ -133,13 +133,11 @@ def run_on_file(runner, filename)
end

def run_using_cache(runner, filename, file_content)
if cache.include?(filename)
runner.restore_offenses(cache.get(filename, file_content))
cache.add_hit(filename) if prune_cache?
if (cache_result_offenses = cache.get(filename, file_content))
runner.restore_offenses(cache_result_offenses)
else
run_with_corrections(runner, filename, file_content)
cache[filename] = runner.offenses.map(&:to_json_format).to_json
cache.add_new_result(filename) if prune_cache?
cache.set(filename, file_content, runner.offenses.map(&:to_cached_offense_json_format).to_json)
end
end

Expand Down
45 changes: 2 additions & 43 deletions lib/erb_lint/offense.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,8 @@ def initialize(linter, source_range, message, context = nil, severity = nil)
@severity = severity
end

def to_json_format
{
linter_config: linter.config.to_hash,
linter_config_class: linter.config.class.name,
linter_class: linter.class.name,
source_range: source_range_to_json_format,
message: message,
context: context,
severity: severity,
}
end

def source_range_to_json_format
{
begin_pos: source_range.begin_pos,
end_pos: source_range.end_pos,
source_buffer_name: source_range.source_buffer.name,
}
end

def self.from_json(parsed_json, config, file_loader, file_content)
parsed_json.transform_keys!(&:to_sym)
linter_config = const_get(parsed_json[:linter_config_class]).new(parsed_json[:linter_config])
linter = const_get(parsed_json[:linter_class]).new(file_loader, linter_config)
new(
linter,
source_range_from_json_format(parsed_json[:source_range], file_content),
parsed_json[:message].presence,
parsed_json[:context].presence,
parsed_json[:severity].presence&.to_sym
)
end

def self.source_range_from_json_format(parsed_json_source_range, file_content)
parsed_json_source_range.transform_keys!(&:to_sym)
Parser::Source::Range.new(
Parser::Source::Buffer.new(
parsed_json_source_range[:source_buffer_name],
source: file_content
),
parsed_json_source_range[:begin_pos],
parsed_json_source_range[:end_pos]
)
def to_cached_offense_json_format
ERBLint::CachedOffense.new_from_offense(self).to_json_format
end

def inspect
Expand Down
54 changes: 27 additions & 27 deletions spec/erb_lint/cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@

describe "#get" do
it "returns a cached lint result" do
cache_result = cache.get(linted_file_path, cache_file_content)
cache_result = cache.get(linted_file_path, linted_file_content)
expect(cache_result.count).to(eq(2))
end
end

describe "#[]=" do
it "caches a lint result" do
cache[linted_file_path] = cache_file_content
cache.set(linted_file_path, linted_file_content, cache_file_content)
expect(File.exist?(
File.join(
cache_dir,
Expand All @@ -68,16 +68,6 @@
end
end

describe "#include?" do
it "returns true if the cache includes the filename" do
expect(cache.include?(linted_file_path)).to(be(true))
end

it "returns false if the cache does not include the filename" do
expect(cache.include?("gibberish")).to(be(false))
end
end

describe "#cache_dir_exists?" do
it "returns true if the cache dir exists" do
expect(cache.cache_dir_exists?).to(be(true))
Expand All @@ -95,15 +85,15 @@
end
end

describe "#prune" do
describe "#prune_cache" do
it "skips prune if no cache hits" do
allow(cache).to(receive(:hits).and_return([]))

expect { cache.prune }.to(output(/Cache being created for the first time, skipping prune/).to_stdout)
expect { cache.prune_cache }.to(output(/Cache being created for the first time, skipping prune/).to_stdout)
end

it "does not prune actual cache hits" do
cache.prune
cache.prune_cache

expect(File.exist?(
File.join(
Expand All @@ -115,12 +105,12 @@

it "does not prune new cache results" do
allow(cache).to(receive(:hits).and_return(["fake-hit"]))
allow(cache).to(receive(:new_results).and_return([linted_file_path]))
allow(cache).to(receive(:new_results).and_return([checksum]))
fakefs_dir = Struct.new(:fakefs_dir)
allow(fakefs_dir).to(receive(:children).and_return([checksum]))
allow(FakeFS::Dir).to(receive(:new).and_return(fakefs_dir))

expect { cache.prune }.to(output(/Skipping deletion of new cache result #{checksum} in prune/).to_stdout)
expect { cache.prune_cache }.to(output(/.*Skipping deletion of new cache result #{checksum}/).to_stdout)

expect(File.exist?(
File.join(
Expand All @@ -140,7 +130,7 @@
f.write(cache_file_content)
end

expect { cache.prune }.to(output(/Cleaning deleted cached file with checksum fake-checksum/).to_stdout)
expect { cache.prune_cache }.to(output(/Cleaning deleted cached file with checksum fake-checksum/).to_stdout)

expect(File.exist?(
File.join(
Expand All @@ -151,19 +141,29 @@
end
end

describe "#add_new_result" do
it "adds new result to cache object new_results attribute" do
cache.add_new_result(linted_file_path)
describe "prune cache mode on #get and #[] behavior" do
before do
allow(cache).to(receive(:prune?).and_return(true))
end

expect(cache.send(:new_results)).to(include(linted_file_path))
it "adds new result to cache object new_results list attribute" do
cache.set(linted_file_path, linted_file_content, cache_file_content)

expect(cache.send(:new_results)).to(include(checksum))
end
end

describe "#add_hit" do
it "adds new cache hit to cache object hits attribute" do
cache.add_hit(linted_file_path)
it "adds new cache hit to cache object hits list attribute" do
cache.get(linted_file_path, linted_file_content)

expect(cache.send(:hits)).to(include(checksum))
end
end

expect(cache.send(:hits)).to(include(linted_file_path))
describe "#close" do
it "Calls prune_cache if prune_cache mode is on" do
allow(cache).to(receive(:prune?).and_return(true))
expect(cache).to(receive(:prune_cache))
cache.close
end
end
end
2 changes: 1 addition & 1 deletion spec/erb_lint/fixtures/cache_file_content
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"linter_config":{"enabled":true,"exclude":[]},"linter_config_class":"ERBLint::LinterConfig","linter_class":"ERBLint::Linters::SpaceAroundErbTag","source_range":{"begin_pos":2,"end_pos":2,"source_buffer_name":"/home/zach/dev/fake-app/app/components/elements/image_component/image_component.html.erb"},"message":"Use 1 space after `<%` instead of 0 space.","context":" ","severity":null},{"linter_config":{"enabled":true,"exclude":[]},"linter_config_class":"ERBLint::LinterConfig","linter_class":"ERBLint::Linters::SpaceAroundErbTag","source_range":{"begin_pos":172,"end_pos":172,"source_buffer_name":"/home/zach/dev/fake-app/app/components/elements/image_component/image_component.html.erb"},"message":"Use 1 space before `%>` instead of 0 space.","context":" ","severity":null}]
[{"message":"Layout/InitialIndentation: Indentation of first line in file detected.","line_number":"1","severity":"convention"},{"message":"Layout/TrailingEmptyLines: Final newline missing.","line_number":"1","severity":"convention"}]