packwerk 1.0.0 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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}
|