Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -80,6 +80,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)
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 @@ -99,7 +119,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]
```

## 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 @@ -35,6 +35,7 @@ module Packwerk
autoload :Package
autoload :PackageSet
autoload :ParsedConstantDefinitions
autoload :FileParser
autoload :Parsers
autoload :ParseRun
autoload :UnresolvedReference
Expand Down
33 changes: 33 additions & 0 deletions lib/packwerk/file_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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])

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

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

sig { abstract.params(io: File, 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
20 changes: 11 additions & 9 deletions lib/packwerk/file_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ def initialize(node_processor_factory:, cache:, parser_factory: nil)
)
end
def call(absolute_file)
parser = parser_for(absolute_file)
return [UnknownFileTypeResult.new(file: absolute_file)] if T.unsafe(parser).nil?
parsers = parsers_for(absolute_file)
return [UnknownFileTypeResult.new(file: absolute_file)] if parsers.empty?

@cache.with_cache(absolute_file) do
node = parse_into_ast(absolute_file, T.must(parser))
return [] unless node
parsers.flat_map do |parser|
node = parse_into_ast(absolute_file, parser)
return [] unless node

references_from_ast(node, absolute_file)
references_from_ast(node, absolute_file)
end
end
rescue Parsers::ParseError => e
[e.result]
Expand All @@ -56,7 +58,7 @@ def call(absolute_file)
private

sig do
params(node: Parser::AST::Node, absolute_file: String).returns(T::Array[UnresolvedReference])
params(node: ::Parser::AST::Node, absolute_file: String).returns(T::Array[UnresolvedReference])
end
def references_from_ast(node, absolute_file)
references = []
Expand All @@ -68,15 +70,15 @@ def references_from_ast(node, absolute_file)
references
end

sig { params(absolute_file: String, parser: Parsers::ParserInterface).returns(T.untyped) }
sig { params(absolute_file: String, parser: Packwerk::FileParser).returns(T.untyped) }
def parse_into_ast(absolute_file, parser)
File.open(absolute_file, "r", nil, external_encoding: Encoding::UTF_8) do |file|
parser.call(io: file, file_path: absolute_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[Packwerk::FileParser]) }
def parsers_for(file_path)
@parser_factory.for_path(file_path)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/packwerk/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def constant_name(constant_node)
def each_child(node)
if block_given?
node.children.each do |child|
yield child if child.is_a?(Parser::AST::Node)
yield child if child.is_a?(::Parser::AST::Node)
end
else
enum_for(:each_child, node)
Expand Down
4 changes: 2 additions & 2 deletions lib/packwerk/node_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def initialize(reference_extractor:, absolute_file:)

sig do
params(
node: Parser::AST::Node,
ancestors: T::Array[Parser::AST::Node]
node: ::Parser::AST::Node,
ancestors: T::Array[::Parser::AST::Node]
).returns(T.nilable(UnresolvedReference))
end
def call(node, ancestors)
Expand Down
13 changes: 10 additions & 3 deletions lib/packwerk/parsers/erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
module Packwerk
module Parsers
class Erb
include ParserInterface
include Packwerk::FileParser

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

def initialize(parser_class: BetterHtml::Parser, ruby_parser: Ruby.new)
@parser_class = parser_class
@ruby_parser = ruby_parser
end

def call(io:, file_path: "<unknown>")
buffer = Parser::Source::Buffer.new(file_path)
buffer = ::Parser::Source::Buffer.new(file_path)
buffer.source = io.read
parse_buffer(buffer, file_path: file_path)
end
Expand All @@ -28,11 +31,15 @@ def parse_buffer(buffer, file_path:)
rescue EncodingError => e
result = ParseResult.new(file: file_path, message: e.message)
raise Parsers::ParseError, result
rescue Parser::SyntaxError => e
rescue ::Parser::SyntaxError => e
result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
raise Parsers::ParseError, result
end

def match?(path:)
ERB_REGEX.match?(path)
end

private

def to_ruby_ast(erb_ast, file_path)
Expand Down
29 changes: 2 additions & 27 deletions lib/packwerk/parsers/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +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 { 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 ||= erb_parser_class.new
end
end

def erb_parser_class
@erb_parser_class ||= Erb
end

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
17 changes: 0 additions & 17 deletions lib/packwerk/parsers/parser_interface.rb

This file was deleted.

22 changes: 17 additions & 5 deletions lib/packwerk/parsers/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,24 @@
module Packwerk
module Parsers
class Ruby
include ParserInterface
include Packwerk::FileParser

class RaiseExceptionsParser < Parser::CurrentRuby
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
def initialize(builder)
super(builder)
super.diagnostics.all_errors_are_fatal = true
end
end

class TolerateInvalidUtf8Builder < Parser::Builders::Default
class TolerateInvalidUtf8Builder < ::Parser::Builders::Default
def string_value(token)
value(token)
end
Expand All @@ -28,17 +36,21 @@ def initialize(parser_class: RaiseExceptionsParser)
end

def call(io:, file_path: "<unknown>")
buffer = Parser::Source::Buffer.new(file_path)
buffer = ::Parser::Source::Buffer.new(file_path)
buffer.source = io.read
parser = @parser_class.new(@builder)
parser.parse(buffer)
rescue EncodingError => e
result = ParseResult.new(file: file_path, message: e.message)
raise Parsers::ParseError, result
rescue Parser::SyntaxError => e
rescue ::Parser::SyntaxError => e
result = ParseResult.new(file: file_path, message: "Syntax error: #{e}")
raise Parsers::ParseError, result
end

def match?(path:)
RUBY_REGEX.match?(path)
end
end
end
end
8 changes: 4 additions & 4 deletions lib/packwerk/reference_extractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def initialize(

sig do
params(
node: Parser::AST::Node,
ancestors: T::Array[Parser::AST::Node],
node: ::Parser::AST::Node,
ancestors: T::Array[::Parser::AST::Node],
absolute_file: String
).returns(T.nilable(UnresolvedReference))
end
Expand Down Expand Up @@ -99,8 +99,8 @@ def self.get_fully_qualified_references_and_offenses_from(unresolved_references_
sig do
params(
constant_name: String,
node: Parser::AST::Node,
ancestors: T::Array[Parser::AST::Node],
node: ::Parser::AST::Node,
ancestors: T::Array[::Parser::AST::Node],
absolute_file: String
).returns(T.nilable(UnresolvedReference))
end
Expand Down
2 changes: 1 addition & 1 deletion test/unit/parsers/erb_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ErbTest < Minitest::Test

test "#call writes parse error to stdout" do
error_message = "stub error"
err = Parser::SyntaxError.new(stub(message: error_message))
err = ::Parser::SyntaxError.new(stub(message: error_message))
parser = stub
parser.stubs(:ast).raises(err)

Expand Down
Loading