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

Configurable parsers (continued) #375

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
22 changes: 21 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ Packwerk reads from the `packwerk.yml` configuration file in the root directory.
| cache | false | when true, caches the results of parsing files |
| cache_directory | tmp/cache/packwerk | the directory that will hold the packwerk cache |

### Using custom parsers

You can specify a custom parser to parse different file formats (e.g. slim or haml)

```ruby
class SlimParser
include Packwerk::FileParser

REGEX = /\.slim\Z/

def call
# Your parsing logic here
end

def match?(path)
richardmarbach marked this conversation as resolved.
Show resolved Hide resolved
REGEX.match?(path)
end
end
```

### Using a custom ERB parser

You can specify a custom ERB parser if needed. For example, if you're using `<%graphql>` tags from https://github.com/github/graphql-client in your ERBs, you can use a custom parser subclass to comment them out so that Packwerk can parse the rest of the file:
Expand All @@ -102,7 +122,7 @@ class CustomParser < Packwerk::Parsers::Erb
end
end

Packwerk::Parsers::Factory.instance.erb_parser_class = CustomParser
Packwerk::Parsers::Factory.instance.parsers = [Packwerk::Parsers::Ruby, CustomParser]
```
richardmarbach marked this conversation as resolved.
Show resolved Hide resolved

## Using the cache
Expand Down
1 change: 1 addition & 0 deletions lib/packwerk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Packwerk
autoload :Configuration
autoload :ConstantContext
autoload :Commands
autoload :FileParser
autoload :Node
autoload :Offense
autoload :OffenseCollection
Expand Down
42 changes: 42 additions & 0 deletions lib/packwerk/file_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# typed: strict
# frozen_string_literal: true

module Packwerk
module FileParser
extend T::Helpers
extend T::Sig

requires_ancestor { Kernel }

interface!

@parsers = T.let([], T::Array[Class])

class << self
extend T::Sig

sig { params(base: Class).void }
def included(base)
@parsers << base
end

sig { returns(T::Array[FileParser]) }
def all
T.unsafe(@parsers).map(&:new)
end

sig { params(base: Class).void }
def remove(base)
@parsers.delete(base)
end
end

sig { abstract.params(io: T.any(IO, StringIO), file_path: String).returns(T.untyped) }
def call(io:, file_path:)
end

sig { abstract.params(path: String).returns(T::Boolean) }
def match?(path:)
end
end
end
18 changes: 10 additions & 8 deletions lib/packwerk/file_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@ class ProcessedFile < T::Struct
params(relative_file: String).returns(ProcessedFile)
end
def call(relative_file)
parser = parser_for(relative_file)
if parser.nil?
parsers = parsers_for(relative_file)
if parsers.empty?
return ProcessedFile.new(offenses: [UnknownFileTypeResult.new(file: relative_file)])
end

unresolved_references = @cache.with_cache(relative_file) do
node = parse_into_ast(relative_file, parser)
return ProcessedFile.new unless node
parsers.flat_map do |parser|
node = parse_into_ast(relative_file, parser)
return ProcessedFile.new unless node

references_from_ast(node, relative_file)
references_from_ast(node, relative_file)
end
end

ProcessedFile.new(unresolved_references: unresolved_references)
Expand Down Expand Up @@ -81,15 +83,15 @@ def references_from_ast(node, relative_file)
references
end

sig { params(relative_file: String, parser: Parsers::ParserInterface).returns(T.untyped) }
sig { params(relative_file: String, parser: FileParser).returns(T.untyped) }
def parse_into_ast(relative_file, parser)
File.open(relative_file, "r", nil, external_encoding: Encoding::UTF_8) do |file|
parser.call(io: file, file_path: relative_file)
end
end

sig { params(file_path: String).returns(T.nilable(Parsers::ParserInterface)) }
def parser_for(file_path)
sig { params(file_path: String).returns(T::Array[FileParser]) }
def parsers_for(file_path)
@parser_factory.for_path(file_path)
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/packwerk/parsers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ module Packwerk
module Parsers
autoload :Erb, "packwerk/parsers/erb"
autoload :Factory, "packwerk/parsers/factory"
autoload :ParserInterface, "packwerk/parsers/parser_interface"
autoload :Ruby, "packwerk/parsers/ruby"

# Require parsers so that they are registered with FileParser
Dir[File.join(__dir__, "parsers", "*.rb")].each { |file| require file }

class ParseResult < Offense; end

class ParseError < StandardError
Expand Down
10 changes: 9 additions & 1 deletion lib/packwerk/parsers/erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ module Parsers
class Erb
extend T::Sig

include ParserInterface
include Packwerk::FileParser

ERB_REGEX = /\.erb\Z/
private_constant :ERB_REGEX

sig { params(parser_class: T.untyped, ruby_parser: Ruby).void }
def initialize(parser_class: BetterHtml::Parser, ruby_parser: Ruby.new)
Expand All @@ -38,6 +41,11 @@ def parse_buffer(buffer, file_path:)
raise Parsers::ParseError, result
end

sig { override.params(path: String).returns(T::Boolean) }
def match?(path:)
ERB_REGEX.match?(path)
end

private

sig do
Expand Down
38 changes: 2 additions & 36 deletions lib/packwerk/parsers/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,9 @@ class Factory
extend T::Sig
include Singleton

RUBY_REGEX = %r{
# Although not important for regex, these are ordered from most likely to match to least likely.
\.(rb|rake|builder|gemspec|ru)\Z
|
(Gemfile|Rakefile)\Z
}x
private_constant :RUBY_REGEX

ERB_REGEX = /\.erb\Z/
private_constant :ERB_REGEX

sig { void }
def initialize
@ruby_parser = T.let(nil, T.nilable(ParserInterface))
@erb_parser = T.let(nil, T.nilable(ParserInterface))
@erb_parser_class = T.let(nil, T.nilable(Class))
end

sig { params(path: String).returns(T.nilable(ParserInterface)) }
sig { params(path: String).returns(T::Array[Packwerk::FileParser]) }
def for_path(path)
case path
when RUBY_REGEX
@ruby_parser ||= Ruby.new
when ERB_REGEX
@erb_parser ||= T.unsafe(erb_parser_class).new
end
end

sig { returns(Class) }
def erb_parser_class
@erb_parser_class ||= Erb
end

sig { params(klass: T.nilable(Class)).void }
def erb_parser_class=(klass)
@erb_parser_class = klass
@erb_parser = nil
Packwerk::FileParser.all.select { |parser| parser.match?(path: path) }
end
end
end
Expand Down
19 changes: 0 additions & 19 deletions lib/packwerk/parsers/parser_interface.rb

This file was deleted.

15 changes: 14 additions & 1 deletion lib/packwerk/parsers/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ module Parsers
class Ruby
extend T::Sig

include ParserInterface
include Packwerk::FileParser

RUBY_REGEX = %r{
# Although not important for regex, these are ordered from most likely to match to least likely.
\.(rb|rake|builder|gemspec|ru)\Z
|
(Gemfile|Rakefile)\Z
}x
private_constant :RUBY_REGEX

class RaiseExceptionsParser < Parser::CurrentRuby
extend T::Sig
Expand Down Expand Up @@ -49,6 +57,11 @@ def call(io:, file_path: "<unknown>")
result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
raise Parsers::ParseError, result
end

sig { override.params(path: String).returns(T::Boolean) }
def match?(path:)
RUBY_REGEX.match?(path)
end
end
end
end
64 changes: 39 additions & 25 deletions test/unit/packwerk/parsers/factory_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,58 @@ module Packwerk
module Parsers
class FactoryTest < Minitest::Test
test "#for_path gives ruby parser for common Ruby paths" do
assert_instance_of(Parsers::Ruby, factory.for_path("foo.rb"))
assert_instance_of(Parsers::Ruby, factory.for_path("relative/path/to/foo.ru"))
assert_instance_of(Parsers::Ruby, factory.for_path("foo.rake"))
assert_instance_of(Parsers::Ruby, factory.for_path("foo.builder"))
assert_instance_of(Parsers::Ruby, factory.for_path("in/repo/gem/foo.gemspec"))
assert_instance_of(Parsers::Ruby, factory.for_path("Gemfile"))
assert_instance_of(Parsers::Ruby, factory.for_path("some/path/Rakefile"))
assert_instance_of(Parsers::Ruby, factory.for_path("foo.rb").first)
assert_instance_of(Parsers::Ruby, factory.for_path("relative/path/to/foo.ru").first)
assert_instance_of(Parsers::Ruby, factory.for_path("foo.rake").first)
assert_instance_of(Parsers::Ruby, factory.for_path("foo.builder").first)
assert_instance_of(Parsers::Ruby, factory.for_path("in/repo/gem/foo.gemspec").first)
assert_instance_of(Parsers::Ruby, factory.for_path("Gemfile").first)
assert_instance_of(Parsers::Ruby, factory.for_path("some/path/Rakefile").first)
end

test "#for_path gives ERB parser for common ERB paths" do
assert_instance_of(Parsers::Erb, factory.for_path("foo.html.erb"))
assert_instance_of(Parsers::Erb, factory.for_path("foo.md.erb"))
assert_instance_of(Parsers::Erb, factory.for_path("/sub/directory/foo.erb"))
assert_instance_of(Parsers::Erb, factory.for_path("foo.html.erb").first)
assert_instance_of(Parsers::Erb, factory.for_path("foo.md.erb").first)
assert_instance_of(Parsers::Erb, factory.for_path("/sub/directory/foo.erb").first)
end

test "#for_path gives multiple parsers for matching paths" do
fake_class_1 = Class.new do
T.unsafe(self).include(Packwerk::FileParser)

fake_class = Class.new do
T.unsafe(self).include(ParserInterface)
def match?(path:)
/\.haml\Z/.match?(path)
end
end

with_erb_parser_class(fake_class) do
assert_instance_of(fake_class, factory.for_path("foo.html.erb"))
fake_class_2 = Class.new do
T.unsafe(self).include(Packwerk::FileParser)

def match?(path:)
/\.haml\Z/.match?(path)
end
end

factories = factory.for_path("foo.haml")
assert_equal(2, factories.size)
assert_instance_of(fake_class_1, factories[0])
assert_instance_of(fake_class_2, factories[1])

Packwerk::FileParser.remove(fake_class_1)
Packwerk::FileParser.remove(fake_class_2)

factories = factory.for_path("foo.haml")
assert_equal(0, factories.size)
end

test "#for_path gives nil for unknown path" do
assert_nil(factory.for_path("not_a_ruby.rb.txt"))
assert_nil(factory.for_path("some/path/rb"))
assert_nil(factory.for_path("compoennts/foo/body.erb.html"))
test "#for_path gives empty array for unknown path" do
assert_empty(factory.for_path("not_a_ruby.rb.txt"))
assert_empty(factory.for_path("some/path/rb"))
assert_empty(factory.for_path("compoennts/foo/body.erb.html"))
end

private

def with_erb_parser_class(klass)
factory.erb_parser_class = klass
yield
ensure
factory.erb_parser_class = nil
end

def factory
Parsers::Factory.instance
end
Expand Down