query_packwerk 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 392d286b38084a21900d3cfe40e81f79f7e6eff5ca872327551589688b309d85
4
- data.tar.gz: 5bc5556fbc5279b3d8056757067ec696e362ab2860251cc6cefc403223f89ce6
3
+ metadata.gz: 98b9a4d914a8be019fa0b1795cd527ac2bfd4ce7931b1e5a204704b5bb503847
4
+ data.tar.gz: 209fa3baaa03c0687daca480890ff32ec6c2111c1c902c3088771614d4127c53
5
5
  SHA512:
6
- metadata.gz: 5f99589b97b415a4f98d404b5270e368b201353ce0b5107f70f3341b43c9f6590cae133a4cc0cdd6decc6eb1c0d67a373fbef15df439beed841653bcec6ac4a0
7
- data.tar.gz: 9ede139265993386cbfbd3e5a9987b00d658865af3d4a3833f0bf41b0d723e84da51c91737b49175a37c52f1668b2671655030ee19a2628e910644416084b7e9
6
+ metadata.gz: 406b5e9039a31dda4264391d1287fa89684dc21a982521c212734c05055e727ecaea62b9ed234e296a151587fd9cbaee5dd99915573f5ccb90429509f12de046
7
+ data.tar.gz: e11dfd82ec7324d499e37c0214d7b12ad735d03732f996bcd8e8397f36f684fd99ff6e3b58fa73a1adbc63f7e87d4be455e62814bb690e51cca0d27d7d8d22dc
data/README.md CHANGED
@@ -27,6 +27,14 @@ The easiest way to use QueryPackwerk is through its interactive console. This wi
27
27
  query_packwerk console
28
28
  ```
29
29
 
30
+ To use the Packwerk context of another directory, pass it as an argument:
31
+
32
+ ```bash
33
+ query_packwerk console path/to/app
34
+ ```
35
+
36
+ Run `query_packwerk console --help` for usage details.
37
+
30
38
  The CLI includes a welcome message with a list of commands. You can access it any time with `welcome`:
31
39
 
32
40
  ```
@@ -116,7 +124,7 @@ QueryPackwerk.anonymous_violation_counts_for("my_pack", threshold: 3)
116
124
 
117
125
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
118
126
 
119
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, push a PR with the label for the version you want, like 'major', 'minor' or 'patch'. When the PR is merged, the version will be automatically released to RubyGems.
127
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, bump `VERSION` in `lib/query_packwerk/version.rb` in your PR and run `bundle install` so `Gemfile.lock` matches. When the PR is merged and CI passes on `main`, the gem is tagged and published to RubyGems automatically. Merges that don't change the version don't publish anything.
120
128
 
121
129
  ## Contributing
122
130
 
@@ -8,12 +8,24 @@ module QueryPackwerk
8
8
  class CLI < Thor
9
9
  extend T::Sig
10
10
 
11
+ # Thor maps --help to its help command only as the first argument, so commands check options[:help].
12
+ class_option :help, type: :boolean, aliases: '-h', desc: 'Show this help message'
13
+ check_unknown_options!
14
+
11
15
  default_command :console
12
16
 
13
- desc 'console',
14
- 'Query packwerk in the current directory via console'
17
+ sig { returns(T::Boolean) }
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
22
+ desc 'console [DIRECTORY]', 'Query packwerk in DIRECTORY via console'
23
+ long_desc 'Query packwerk in DIRECTORY via console. DIRECTORY defaults to the current directory.'
15
24
  sig { params(directory: String).void }
16
25
  def console(directory = Dir.pwd)
26
+ return help('console') if options[:help]
27
+ raise Thor::Error, "Could not find directory #{directory.inspect}." unless File.directory?(directory)
28
+
17
29
  QueryPackwerk::Console.start(directory)
18
30
  end
19
31
  end
@@ -9,7 +9,9 @@ module QueryPackwerk
9
9
  class FileCache
10
10
  extend T::Sig
11
11
 
12
- RUBY_FILE = T.let(/\.(rb|rake)\z/, Regexp)
12
+ # The files packwerk parses as Ruby, from Packwerk::Parsers::Factory
13
+ RUBY_FILE = T.let(/(\.(rb|rake|builder|gemspec|ru)|Gemfile|Rakefile)\z/, Regexp)
14
+ ERB_FILE = T.let(/\.erb\z/, Regexp)
13
15
 
14
16
  sig { void }
15
17
  def initialize
@@ -62,12 +64,18 @@ module QueryPackwerk
62
64
 
63
65
  sig { params(file_name: String).returns(RuboCop::AST::Node) }
64
66
  def get_file_ast(file_name)
65
- @file_ast_cache[file_name] ||= ast_from(get_file(file_name))
67
+ @file_ast_cache[file_name] ||= begin
68
+ source = processed_source(ruby_source(file_name))
69
+ warn_unparseable(file_name, source) unless source.valid_syntax?
70
+
71
+ # The AST is also nil for source with no code, like a template that's only HTML
72
+ source.ast || ast_from('x')
73
+ end
66
74
  end
67
75
 
68
76
  sig { params(file_name: String).returns(String) }
69
77
  def get_file(file_name)
70
- @file_cache[file_name] ||= if RUBY_FILE.match?(file_name) && File.exist?(file_name)
78
+ @file_cache[file_name] ||= if (RUBY_FILE.match?(file_name) || ERB_FILE.match?(file_name)) && File.exist?(file_name)
71
79
  File.read(file_name)
72
80
  else
73
81
  'x'
@@ -84,7 +92,7 @@ module QueryPackwerk
84
92
  absolute_const_node = ast_from(class_name)
85
93
  relative_const_node = ast_from(class_name.delete_prefix('::'))
86
94
 
87
- @file_const_cache[const_key] = get_file_ast(file_name).each_descendant.select do |node|
95
+ @file_const_cache[const_key] = get_file_ast(file_name).each_node.select do |node|
88
96
  next false unless node.const_type?
89
97
 
90
98
  node == absolute_const_node || node == relative_const_node
@@ -133,10 +141,32 @@ module QueryPackwerk
133
141
 
134
142
  private
135
143
 
144
+ # Blanks out everything in an ERB template except its Ruby code, so line numbers still match the template
145
+ sig { params(file_name: String).returns(String) }
146
+ def ruby_source(file_name)
147
+ source = get_file(file_name)
148
+ return source unless ERB_FILE.match?(file_name)
149
+
150
+ Herb.extract_ruby(source, semicolons: true, comments: false, preserve_positions: true)
151
+ end
152
+
153
+ sig { params(string: String).returns(RuboCop::ProcessedSource) }
154
+ def processed_source(string)
155
+ RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
156
+ end
157
+
158
+ sig { params(file_name: String, source: RuboCop::ProcessedSource).void }
159
+ def warn_unparseable(file_name, source)
160
+ error = source.diagnostics.find { |diagnostic| diagnostic.level == :error }
161
+ location = error ? "#{file_name}:#{error.location.line} (#{error.message})" : file_name
162
+
163
+ warn "Could not parse #{location}, so its violations are not included"
164
+ end
165
+
136
166
  # Turns a string into a Ruby AST
137
167
  sig { params(string: String).returns(RuboCop::AST::Node) }
138
168
  def ast_from(string)
139
- RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f).ast
169
+ processed_source(string).ast
140
170
  end
141
171
 
142
172
  # We can find a constant, but by going up its parents we can find the full call chain.
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module QueryPackwerk
5
- VERSION = '0.1.24'
5
+ VERSION = '0.1.26'
6
6
  end
@@ -9,8 +9,6 @@ module QueryPackwerk
9
9
  class Violation
10
10
  extend T::Sig
11
11
 
12
- # This does not play nicely with ERB files which may have violations
13
- RUBY_FILE = T.let(/\.(rb|rake)\z/, Regexp)
14
12
  ALL_CAPS = T.let(/\A[A-Z_]+\z/, Regexp)
15
13
 
16
14
  sig { returns(QueryPackwerk::Package) }
@@ -168,13 +166,15 @@ module QueryPackwerk
168
166
  load_cache!
169
167
 
170
168
  file_sources = files.flat_map do |file_name|
169
+ lines = @file_cache.get_file(file_name).lines
170
+
171
171
  @file_cache.get_full_sources(file_name: file_name, class_name: class_name).map do |s|
172
172
  line_number = s.loc.line
173
- start_pos = line_number - start_offset
174
- end_pos = line_number + end_offset
173
+ start_pos = [line_number - start_offset, 1].max
174
+ end_pos = [line_number + end_offset, lines.size].min
175
175
 
176
176
  location = "#{file_name}:#{s.loc.line} (L#{start_pos}..#{end_pos})"
177
- context = @file_cache.get_file(file_name).lines.slice(start_pos..end_pos)
177
+ context = lines[(start_pos - 1)...end_pos]
178
178
  full_context = unindent((context || ['']).join)
179
179
 
180
180
  [@file_cache.anonymize_arguments(s.source), "> #{location}\n\n#{full_context}"]
@@ -192,7 +192,9 @@ module QueryPackwerk
192
192
  def sources_with_contexts(start_offset: 3, end_offset: 3)
193
193
  load_sources!
194
194
 
195
- deep_merge_hash_groups(@original_collection) { |v| [v.class_name, v.sources_with_contexts] }
195
+ deep_merge_hash_groups(@original_collection) do |v|
196
+ [v.class_name, v.sources_with_contexts(start_offset:, end_offset:)]
197
+ end
196
198
  end
197
199
 
198
200
  sig { params(start_offset: Integer, end_offset: Integer).returns(String) }
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'sorbet-runtime'
5
+ require 'herb'
5
6
  require 'parse_packwerk'
6
7
  require 'rubocop'
7
8
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: query_packwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.24
4
+ version: 0.1.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineering
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: herb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.9'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: packwerk
28
42
  requirement: !ruby/object:Gem::Requirement