packwerk 1.0.0 → 1.1.2
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 +4 -4
- data/.github/pull_request_template.md +8 -7
- data/.github/workflows/ci.yml +1 -1
- data/.gitignore +1 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -2
- data/README.md +5 -3
- data/TROUBLESHOOT.md +1 -1
- data/USAGE.md +56 -19
- data/exe/packwerk +1 -1
- data/lib/packwerk.rb +3 -3
- data/lib/packwerk/application_load_paths.rb +68 -0
- data/lib/packwerk/application_validator.rb +96 -70
- data/lib/packwerk/association_inspector.rb +50 -20
- data/lib/packwerk/cache_deprecated_references.rb +55 -0
- data/lib/packwerk/checker.rb +23 -0
- data/lib/packwerk/checking_deprecated_references.rb +5 -2
- data/lib/packwerk/cli.rb +65 -56
- data/lib/packwerk/commands/detect_stale_violations_command.rb +60 -0
- data/lib/packwerk/commands/offense_progress_marker.rb +24 -0
- data/lib/packwerk/commands/result.rb +13 -0
- data/lib/packwerk/commands/update_deprecations_command.rb +81 -0
- data/lib/packwerk/configuration.rb +6 -34
- data/lib/packwerk/const_node_inspector.rb +28 -17
- data/lib/packwerk/dependency_checker.rb +16 -5
- data/lib/packwerk/deprecated_references.rb +24 -1
- data/lib/packwerk/detect_stale_deprecated_references.rb +14 -0
- data/lib/packwerk/file_processor.rb +4 -4
- data/lib/packwerk/formatters/offenses_formatter.rb +48 -0
- data/lib/packwerk/formatters/progress_formatter.rb +6 -2
- data/lib/packwerk/generators/application_validation.rb +2 -2
- data/lib/packwerk/generators/templates/package.yml +4 -0
- data/lib/packwerk/generators/templates/packwerk +2 -2
- data/lib/packwerk/generators/templates/packwerk.yml.erb +1 -1
- data/lib/packwerk/inflector.rb +17 -8
- data/lib/packwerk/node.rb +78 -39
- data/lib/packwerk/node_processor.rb +14 -3
- data/lib/packwerk/node_processor_factory.rb +39 -0
- data/lib/packwerk/offense.rb +4 -6
- data/lib/packwerk/output_style.rb +20 -0
- data/lib/packwerk/output_styles/coloured.rb +29 -0
- data/lib/packwerk/output_styles/plain.rb +26 -0
- data/lib/packwerk/package.rb +8 -1
- data/lib/packwerk/package_set.rb +13 -5
- data/lib/packwerk/parsed_constant_definitions.rb +4 -4
- data/lib/packwerk/parsers/erb.rb +4 -0
- data/lib/packwerk/parsers/factory.rb +10 -1
- data/lib/packwerk/privacy_checker.rb +26 -5
- data/lib/packwerk/run_context.rb +70 -46
- data/lib/packwerk/sanity_checker.rb +1 -1
- data/lib/packwerk/spring_command.rb +1 -1
- data/lib/packwerk/updating_deprecated_references.rb +2 -39
- data/lib/packwerk/version.rb +1 -1
- data/packwerk.gemspec +2 -2
- metadata +15 -8
- data/lib/packwerk/output_styles.rb +0 -41
- data/static/packwerk-check-demo.png +0 -0
- data/static/packwerk_check.gif +0 -0
- data/static/packwerk_check_violation.gif +0 -0
- data/static/packwerk_update.gif +0 -0
- data/static/packwerk_validate.gif +0 -0
@@ -35,7 +35,7 @@ module Packwerk
|
|
35
35
|
return true
|
36
36
|
end
|
37
37
|
|
38
|
-
source_file_path = File.expand_path("
|
38
|
+
source_file_path = File.expand_path("templates/packwerk", __dir__)
|
39
39
|
FileUtils.cp(source_file_path, destination_file_path)
|
40
40
|
|
41
41
|
@out.puts("✅ Packwerk application validation bin script generated in #{destination_file_path}")
|
@@ -51,7 +51,7 @@ module Packwerk
|
|
51
51
|
return true
|
52
52
|
end
|
53
53
|
|
54
|
-
source_file_path = File.expand_path("
|
54
|
+
source_file_path = File.expand_path("templates/packwerk_validator_test.rb", __dir__)
|
55
55
|
FileUtils.cp(source_file_path, destination_file_path)
|
56
56
|
|
57
57
|
@out.puts("✅ Packwerk application validation test generated in #{destination_file_path}")
|
@@ -11,6 +11,10 @@ enforce_dependencies: true
|
|
11
11
|
# We recommend enabling this for any new packages you create to aid with encapsulation.
|
12
12
|
enforce_privacy: false
|
13
13
|
|
14
|
+
# By default the public path will be app/public/, however this may not suit all applications' architecture so
|
15
|
+
# this allows you to modify what your package's public path is.
|
16
|
+
# public_path: app/public/
|
17
|
+
|
14
18
|
# A list of this package's dependencies
|
15
19
|
# Note that packages in this list require their own `package.yml` file
|
16
20
|
# dependencies:
|
@@ -10,12 +10,12 @@ ENV["RAILS_ENV"] = "test"
|
|
10
10
|
packwerk_argv = ARGV.dup
|
11
11
|
|
12
12
|
begin
|
13
|
-
load(File.expand_path("
|
13
|
+
load(File.expand_path("spring", __dir__))
|
14
14
|
rescue LoadError => e
|
15
15
|
raise unless e.message.include?("spring")
|
16
16
|
end
|
17
17
|
|
18
|
-
require File.expand_path("
|
18
|
+
require File.expand_path("../config/environment", __dir__)
|
19
19
|
|
20
20
|
require "packwerk"
|
21
21
|
|
data/lib/packwerk/inflector.rb
CHANGED
@@ -8,20 +8,31 @@ require "packwerk/inflections/custom"
|
|
8
8
|
module Packwerk
|
9
9
|
class Inflector
|
10
10
|
class << self
|
11
|
+
extend T::Sig
|
12
|
+
|
11
13
|
def default
|
12
|
-
@default ||= new
|
14
|
+
@default ||= new(custom_inflector: Inflections::Custom.new)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(inflections_file: String).returns(::Packwerk::Inflector) }
|
18
|
+
def from_file(inflections_file)
|
19
|
+
new(custom_inflector: Inflections::Custom.new(inflections_file))
|
13
20
|
end
|
14
21
|
end
|
15
22
|
|
16
|
-
|
17
|
-
include ::ActiveSupport::Inflector
|
23
|
+
extend T::Sig
|
24
|
+
include ::ActiveSupport::Inflector # For #camelize, #classify, #pluralize, #singularize
|
18
25
|
|
19
|
-
|
26
|
+
sig do
|
27
|
+
params(
|
28
|
+
custom_inflector: Inflections::Custom
|
29
|
+
).void
|
30
|
+
end
|
31
|
+
def initialize(custom_inflector:)
|
20
32
|
@inflections = ::ActiveSupport::Inflector::Inflections.new
|
21
33
|
|
22
34
|
Inflections::Default.apply_to(@inflections)
|
23
|
-
|
24
|
-
Inflections::Custom.new(custom_inflection_file).apply_to(@inflections)
|
35
|
+
custom_inflector.apply_to(@inflections)
|
25
36
|
end
|
26
37
|
|
27
38
|
def pluralize(word, count = nil)
|
@@ -32,8 +43,6 @@ module Packwerk
|
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
35
|
-
private
|
36
|
-
|
37
46
|
def inflections(_ = nil)
|
38
47
|
@inflections
|
39
48
|
end
|
data/lib/packwerk/node.rb
CHANGED
@@ -5,24 +5,14 @@ require "parser/ast/node"
|
|
5
5
|
|
6
6
|
module Packwerk
|
7
7
|
module Node
|
8
|
-
BLOCK = :block
|
9
|
-
CLASS = :class
|
10
|
-
CONSTANT = :const
|
11
|
-
CONSTANT_ASSIGNMENT = :casgn
|
12
|
-
CONSTANT_ROOT_NAMESPACE = :cbase
|
13
|
-
HASH = :hash
|
14
|
-
HASH_PAIR = :pair
|
15
|
-
METHOD_CALL = :send
|
16
|
-
MODULE = :module
|
17
|
-
STRING = :str
|
18
|
-
SYMBOL = :sym
|
19
|
-
|
20
8
|
class TypeError < ArgumentError; end
|
21
9
|
Location = Struct.new(:line, :column)
|
22
10
|
|
23
11
|
class << self
|
12
|
+
extend T::Sig
|
13
|
+
|
24
14
|
def class_or_module_name(class_or_module_node)
|
25
|
-
case
|
15
|
+
case type_of(class_or_module_node)
|
26
16
|
when CLASS, MODULE
|
27
17
|
# (class (const nil :Foo) (const nil :Bar) (nil))
|
28
18
|
# "class Foo < Bar; end"
|
@@ -36,10 +26,10 @@ module Packwerk
|
|
36
26
|
end
|
37
27
|
|
38
28
|
def constant_name(constant_node)
|
39
|
-
case
|
29
|
+
case type_of(constant_node)
|
40
30
|
when CONSTANT_ROOT_NAMESPACE
|
41
31
|
""
|
42
|
-
when CONSTANT, CONSTANT_ASSIGNMENT
|
32
|
+
when CONSTANT, CONSTANT_ASSIGNMENT, SELF
|
43
33
|
# (const nil :Foo)
|
44
34
|
# "Foo"
|
45
35
|
# (const (cbase) :Foo)
|
@@ -52,6 +42,8 @@ module Packwerk
|
|
52
42
|
# "::Foo = 1"
|
53
43
|
# (casgn (lvar :a) :Foo (int 1))
|
54
44
|
# "a::Foo = 1"
|
45
|
+
# (casgn (self) :Foo (int 1))
|
46
|
+
# "self::Foo = 1"
|
55
47
|
namespace, name = constant_node.children
|
56
48
|
if namespace
|
57
49
|
[constant_name(namespace), name].join("::")
|
@@ -74,19 +66,19 @@ module Packwerk
|
|
74
66
|
end
|
75
67
|
|
76
68
|
def enclosing_namespace_path(starting_node, ancestors:)
|
77
|
-
ancestors.select { |n| [CLASS, MODULE].include?(
|
69
|
+
ancestors.select { |n| [CLASS, MODULE].include?(type_of(n)) }
|
78
70
|
.each_with_object([]) do |node, namespace|
|
79
71
|
# when evaluating `class Child < Parent`, the const node for `Parent` is a child of the class
|
80
72
|
# node, so it'll be an ancestor, but `Parent` is not evaluated in the namespace of `Child`, so
|
81
73
|
# we need to skip it here
|
82
|
-
next if
|
74
|
+
next if type_of(node) == CLASS && parent_class(node) == starting_node
|
83
75
|
|
84
76
|
namespace.prepend(class_or_module_name(node))
|
85
77
|
end
|
86
78
|
end
|
87
79
|
|
88
80
|
def literal_value(string_or_symbol_node)
|
89
|
-
case
|
81
|
+
case type_of(string_or_symbol_node)
|
90
82
|
when STRING, SYMBOL
|
91
83
|
# (str "foo")
|
92
84
|
# "'foo'"
|
@@ -103,8 +95,36 @@ module Packwerk
|
|
103
95
|
Location.new(location.line, location.column)
|
104
96
|
end
|
105
97
|
|
98
|
+
def constant?(node)
|
99
|
+
type_of(node) == CONSTANT
|
100
|
+
end
|
101
|
+
|
102
|
+
def constant_assignment?(node)
|
103
|
+
type_of(node) == CONSTANT_ASSIGNMENT
|
104
|
+
end
|
105
|
+
|
106
|
+
def class?(node)
|
107
|
+
type_of(node) == CLASS
|
108
|
+
end
|
109
|
+
|
110
|
+
def method_call?(node)
|
111
|
+
type_of(node) == METHOD_CALL
|
112
|
+
end
|
113
|
+
|
114
|
+
def hash?(node)
|
115
|
+
type_of(node) == HASH
|
116
|
+
end
|
117
|
+
|
118
|
+
def string?(node)
|
119
|
+
type_of(node) == STRING
|
120
|
+
end
|
121
|
+
|
122
|
+
def symbol?(node)
|
123
|
+
type_of(node) == SYMBOL
|
124
|
+
end
|
125
|
+
|
106
126
|
def method_arguments(method_call_node)
|
107
|
-
raise TypeError unless
|
127
|
+
raise TypeError unless method_call?(method_call_node)
|
108
128
|
|
109
129
|
# (send (lvar :foo) :bar (int 1))
|
110
130
|
# "foo.bar(1)"
|
@@ -112,7 +132,7 @@ module Packwerk
|
|
112
132
|
end
|
113
133
|
|
114
134
|
def method_name(method_call_node)
|
115
|
-
raise TypeError unless
|
135
|
+
raise TypeError unless method_call?(method_call_node)
|
116
136
|
|
117
137
|
# (send (lvar :foo) :bar (int 1))
|
118
138
|
# "foo.bar(1)"
|
@@ -120,7 +140,7 @@ module Packwerk
|
|
120
140
|
end
|
121
141
|
|
122
142
|
def module_name_from_definition(node)
|
123
|
-
case
|
143
|
+
case type_of(node)
|
124
144
|
when CLASS, MODULE
|
125
145
|
# "class My::Class; end"
|
126
146
|
# "module My::Module; end"
|
@@ -130,7 +150,7 @@ module Packwerk
|
|
130
150
|
# "My::Module = ..."
|
131
151
|
rvalue = node.children.last
|
132
152
|
|
133
|
-
case
|
153
|
+
case type_of(rvalue)
|
134
154
|
when METHOD_CALL
|
135
155
|
# "Class.new"
|
136
156
|
# "Module.new"
|
@@ -153,16 +173,17 @@ module Packwerk
|
|
153
173
|
end
|
154
174
|
|
155
175
|
def parent_class(class_node)
|
156
|
-
raise TypeError unless
|
176
|
+
raise TypeError unless type_of(class_node) == CLASS
|
157
177
|
|
158
178
|
# (class (const nil :Foo) (const nil :Bar) (nil))
|
159
179
|
# "class Foo < Bar; end"
|
160
180
|
class_node.children[1]
|
161
181
|
end
|
162
182
|
|
183
|
+
sig { params(ancestors: T::Array[AST::Node]).returns(String) }
|
163
184
|
def parent_module_name(ancestors:)
|
164
185
|
definitions = ancestors
|
165
|
-
.select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(
|
186
|
+
.select { |n| [CLASS, MODULE, CONSTANT_ASSIGNMENT, BLOCK].include?(type_of(n)) }
|
166
187
|
|
167
188
|
names = definitions.map do |definition|
|
168
189
|
name_part_from_definition(definition)
|
@@ -171,20 +192,38 @@ module Packwerk
|
|
171
192
|
names.empty? ? "Object" : names.reverse.join("::")
|
172
193
|
end
|
173
194
|
|
174
|
-
def type(node)
|
175
|
-
node.type
|
176
|
-
end
|
177
|
-
|
178
195
|
def value_from_hash(hash_node, key)
|
179
|
-
raise TypeError unless
|
196
|
+
raise TypeError unless hash?(hash_node)
|
180
197
|
pair = hash_pairs(hash_node).detect { |pair_node| literal_value(hash_pair_key(pair_node)) == key }
|
181
198
|
hash_pair_value(pair) if pair
|
182
199
|
end
|
183
200
|
|
184
201
|
private
|
185
202
|
|
203
|
+
BLOCK = :block
|
204
|
+
CLASS = :class
|
205
|
+
CONSTANT = :const
|
206
|
+
CONSTANT_ASSIGNMENT = :casgn
|
207
|
+
CONSTANT_ROOT_NAMESPACE = :cbase
|
208
|
+
HASH = :hash
|
209
|
+
HASH_PAIR = :pair
|
210
|
+
METHOD_CALL = :send
|
211
|
+
MODULE = :module
|
212
|
+
SELF = :self
|
213
|
+
STRING = :str
|
214
|
+
SYMBOL = :sym
|
215
|
+
|
216
|
+
private_constant(
|
217
|
+
:BLOCK, :CLASS, :CONSTANT, :CONSTANT_ASSIGNMENT, :CONSTANT_ROOT_NAMESPACE, :HASH, :HASH_PAIR, :METHOD_CALL,
|
218
|
+
:MODULE, :SELF, :STRING, :SYMBOL,
|
219
|
+
)
|
220
|
+
|
221
|
+
def type_of(node)
|
222
|
+
node.type
|
223
|
+
end
|
224
|
+
|
186
225
|
def hash_pair_key(hash_pair_node)
|
187
|
-
raise TypeError unless
|
226
|
+
raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
|
188
227
|
|
189
228
|
# (pair (int 1) (int 2))
|
190
229
|
# "1 => 2"
|
@@ -194,7 +233,7 @@ module Packwerk
|
|
194
233
|
end
|
195
234
|
|
196
235
|
def hash_pair_value(hash_pair_node)
|
197
|
-
raise TypeError unless
|
236
|
+
raise TypeError unless type_of(hash_pair_node) == HASH_PAIR
|
198
237
|
|
199
238
|
# (pair (int 1) (int 2))
|
200
239
|
# "1 => 2"
|
@@ -204,15 +243,15 @@ module Packwerk
|
|
204
243
|
end
|
205
244
|
|
206
245
|
def hash_pairs(hash_node)
|
207
|
-
raise TypeError unless
|
246
|
+
raise TypeError unless hash?(hash_node)
|
208
247
|
|
209
248
|
# (hash (pair (int 1) (int 2)) (pair (int 3) (int 4)))
|
210
249
|
# "{1 => 2, 3 => 4}"
|
211
|
-
hash_node.children.select { |n|
|
250
|
+
hash_node.children.select { |n| type_of(n) == HASH_PAIR }
|
212
251
|
end
|
213
252
|
|
214
253
|
def method_call_node(block_node)
|
215
|
-
raise TypeError unless
|
254
|
+
raise TypeError unless type_of(block_node) == BLOCK
|
216
255
|
|
217
256
|
# (block (send (lvar :foo) :bar) (args) (int 42))
|
218
257
|
# "foo.bar do 42 end"
|
@@ -222,8 +261,8 @@ module Packwerk
|
|
222
261
|
def module_creation?(node)
|
223
262
|
# "Class.new"
|
224
263
|
# "Module.new"
|
225
|
-
|
226
|
-
|
264
|
+
method_call?(node) &&
|
265
|
+
constant?(receiver(node)) &&
|
227
266
|
["Class", "Module"].include?(constant_name(receiver(node))) &&
|
228
267
|
method_name(node) == :new
|
229
268
|
end
|
@@ -231,12 +270,12 @@ module Packwerk
|
|
231
270
|
def name_from_block_definition(node)
|
232
271
|
if method_name(method_call_node(node)) == :class_eval
|
233
272
|
receiver = receiver(node)
|
234
|
-
constant_name(receiver) if receiver &&
|
273
|
+
constant_name(receiver) if receiver && constant?(receiver)
|
235
274
|
end
|
236
275
|
end
|
237
276
|
|
238
277
|
def name_part_from_definition(node)
|
239
|
-
case
|
278
|
+
case type_of(node)
|
240
279
|
when CLASS, MODULE, CONSTANT_ASSIGNMENT
|
241
280
|
module_name_from_definition(node)
|
242
281
|
when BLOCK
|
@@ -245,7 +284,7 @@ module Packwerk
|
|
245
284
|
end
|
246
285
|
|
247
286
|
def receiver(method_call_or_block_node)
|
248
|
-
case
|
287
|
+
case type_of(method_call_or_block_node)
|
249
288
|
when METHOD_CALL
|
250
289
|
method_call_or_block_node.children[0]
|
251
290
|
when BLOCK
|
@@ -3,9 +3,21 @@
|
|
3
3
|
|
4
4
|
require "packwerk/node"
|
5
5
|
require "packwerk/offense"
|
6
|
+
require "packwerk/checker"
|
7
|
+
require "packwerk/reference_lister"
|
6
8
|
|
7
9
|
module Packwerk
|
8
10
|
class NodeProcessor
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig do
|
14
|
+
params(
|
15
|
+
reference_extractor: ReferenceExtractor,
|
16
|
+
reference_lister: ReferenceLister,
|
17
|
+
filename: String,
|
18
|
+
checkers: T::Array[Checker]
|
19
|
+
).void
|
20
|
+
end
|
9
21
|
def initialize(reference_extractor:, reference_lister:, filename:, checkers:)
|
10
22
|
@reference_extractor = reference_extractor
|
11
23
|
@reference_lister = reference_lister
|
@@ -14,8 +26,7 @@ module Packwerk
|
|
14
26
|
end
|
15
27
|
|
16
28
|
def call(node, ancestors:)
|
17
|
-
|
18
|
-
when Node::METHOD_CALL, Node::CONSTANT
|
29
|
+
if Node.method_call?(node) || Node.constant?(node)
|
19
30
|
reference = @reference_extractor.reference_from_node(node, ancestors: ancestors, file_path: @filename)
|
20
31
|
check_reference(reference, node) if reference
|
21
32
|
end
|
@@ -30,7 +41,7 @@ module Packwerk
|
|
30
41
|
|
31
42
|
Packwerk::Offense.new(
|
32
43
|
location: Node.location(node),
|
33
|
-
file:
|
44
|
+
file: reference.relative_path,
|
34
45
|
message: <<~EOS
|
35
46
|
#{message}
|
36
47
|
Inference details: this is a reference to #{constant.name} which seems to be defined in #{constant.location}.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "packwerk/constant_name_inspector"
|
5
|
+
require "packwerk/checker"
|
6
|
+
|
7
|
+
module Packwerk
|
8
|
+
class NodeProcessorFactory < T::Struct
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
const :root_path, String
|
12
|
+
const :reference_lister, ReferenceLister
|
13
|
+
const :context_provider, Packwerk::ConstantDiscovery
|
14
|
+
const :constant_name_inspectors, T::Array[ConstantNameInspector]
|
15
|
+
const :checkers, T::Array[Checker]
|
16
|
+
|
17
|
+
sig { params(filename: String, node: AST::Node).returns(NodeProcessor) }
|
18
|
+
def for(filename:, node:)
|
19
|
+
::Packwerk::NodeProcessor.new(
|
20
|
+
reference_extractor: reference_extractor(node: node),
|
21
|
+
reference_lister: reference_lister,
|
22
|
+
filename: filename,
|
23
|
+
checkers: checkers,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { params(node: AST::Node).returns(::Packwerk::ReferenceExtractor) }
|
30
|
+
def reference_extractor(node:)
|
31
|
+
::Packwerk::ReferenceExtractor.new(
|
32
|
+
context_provider: context_provider,
|
33
|
+
constant_name_inspectors: constant_name_inspectors,
|
34
|
+
root_node: node,
|
35
|
+
root_path: root_path,
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/packwerk/offense.rb
CHANGED
@@ -4,7 +4,8 @@
|
|
4
4
|
require "parser/source/map"
|
5
5
|
require "sorbet-runtime"
|
6
6
|
|
7
|
-
require "packwerk/
|
7
|
+
require "packwerk/output_style"
|
8
|
+
require "packwerk/output_styles/plain"
|
8
9
|
|
9
10
|
module Packwerk
|
10
11
|
class Offense
|
@@ -23,11 +24,8 @@ module Packwerk
|
|
23
24
|
@message = message
|
24
25
|
end
|
25
26
|
|
26
|
-
sig
|
27
|
-
|
28
|
-
.returns(String)
|
29
|
-
end
|
30
|
-
def to_s(style = OutputStyles::Plain)
|
27
|
+
sig { params(style: OutputStyle).returns(String) }
|
28
|
+
def to_s(style = OutputStyles::Plain.new)
|
31
29
|
if location
|
32
30
|
<<~EOS
|
33
31
|
#{style.filename}#{file}#{style.reset}:#{location.line}:#{location.column}
|