Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 720674f

Browse files
committed
Better error messages in the CLI
1 parent 8686c19 commit 720674f

File tree

3 files changed

+117
-32
lines changed

3 files changed

+117
-32
lines changed

lib/syntax_tree.rb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ class SyntaxTree < Ripper
2626
# every character in the string is 1 byte in length, so we can just return the
2727
# start of the line + the index.
2828
class SingleByteString
29+
attr_reader :start
30+
2931
def initialize(start)
3032
@start = start
3133
end
3234

3335
def [](byteindex)
34-
@start + byteindex
36+
start + byteindex
3537
end
3638
end
3739

@@ -40,7 +42,10 @@ def [](byteindex)
4042
# an array of indices, such that array[byteindex] will be equal to the index
4143
# of the character within the string.
4244
class MultiByteString
45+
attr_reader :start, :indices
46+
4347
def initialize(start, line)
48+
@start = start
4449
@indices = []
4550

4651
line.each_char.with_index(start) do |char, index|
@@ -52,7 +57,7 @@ def initialize(start, line)
5257
# there's a BOM at the beginning of the file, which is the reason we need to
5358
# compare it to 0 here.
5459
def [](byteindex)
55-
@indices[byteindex < 0 ? 0 : byteindex]
60+
indices[byteindex < 0 ? 0 : byteindex]
5661
end
5762
end
5863

@@ -186,6 +191,10 @@ def parents
186191
# [Array[ String ]] the list of lines in the source
187192
attr_reader :lines
188193

194+
# [Array[ SingleByteString | MultiByteString ]] the list of objects that
195+
# represent the start of each line in character offsets
196+
attr_reader :line_counts
197+
189198
# [Array[ untyped ]] a running list of tokens that have been found in the
190199
# source. This list changes a lot as certain nodes will "consume" these tokens
191200
# to determine their bounds.
@@ -299,7 +308,7 @@ def self.format(source)
299308
# this line, then we add the number of columns into this line that we've gone
300309
# through.
301310
def char_pos
302-
@line_counts[lineno - 1][column]
311+
line_counts[lineno - 1][column]
303312
end
304313

305314
# As we build up a list of tokens, we'll periodically need to go backwards and
@@ -313,7 +322,7 @@ def char_pos
313322
# (which would happen to be the innermost keyword). Then the outer one would
314323
# only be able to grab the first one. In this way all of the tokens act as
315324
# their own stack.
316-
def find_token(type, value = :any, consume: true)
325+
def find_token(type, value = :any, consume: true, location: nil)
317326
index =
318327
tokens.rindex do |token|
319328
token.is_a?(type) && (value == :any || (token.value == value))
@@ -326,8 +335,16 @@ def find_token(type, value = :any, consume: true)
326335
# could also be caused by accidentally attempting to consume a token twice
327336
# by two different parser event handlers.
328337
unless index
329-
message = "Cannot find expected #{value == :any ? type : value}"
330-
raise ParseError.new(message, lineno, column)
338+
token = value == :any ? type.name.split("::", 2).last : value
339+
message = "Cannot find expected #{token}"
340+
341+
if location
342+
lineno = location.start_line
343+
column = location.start_char - line_counts[lineno - 1].start
344+
raise ParseError.new(message, lineno, column)
345+
else
346+
raise ParseError.new(message, lineno, column)
347+
end
331348
end
332349

333350
tokens.delete_at(index)
@@ -1552,7 +1569,8 @@ def on_array(contents)
15521569
location: lbracket.location.to(rbracket.location)
15531570
)
15541571
else
1555-
tstring_end = find_token(TStringEnd)
1572+
tstring_end =
1573+
find_token(TStringEnd, location: contents.beginning.location)
15561574

15571575
contents.class.new(
15581576
beginning: contents.beginning,
@@ -5195,7 +5213,7 @@ def on_dyna_symbol(string_content)
51955213
if find_token(SymBeg, consume: false)
51965214
# A normal dynamic symbol
51975215
symbeg = find_token(SymBeg)
5198-
tstring_end = find_token(TStringEnd)
5216+
tstring_end = find_token(TStringEnd, location: symbeg.location)
51995217

52005218
DynaSymbol.new(
52015219
quote: symbeg.value,
@@ -11291,7 +11309,7 @@ def on_string_literal(string)
1129111309
)
1129211310
else
1129311311
tstring_beg = find_token(TStringBeg)
11294-
tstring_end = find_token(TStringEnd)
11312+
tstring_end = find_token(TStringEnd, location: tstring_beg.location)
1129511313

1129611314
location =
1129711315
Location.new(
@@ -13503,7 +13521,7 @@ def on_xstring_literal(xstring)
1350313521
location: heredoc.location
1350413522
)
1350513523
else
13506-
ending = find_token(TStringEnd)
13524+
ending = find_token(TStringEnd, location: xstring.location)
1350713525

1350813526
XStringLiteral.new(
1350913527
parts: xstring.parts,

lib/syntax_tree/cli.rb

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,28 @@
33
class SyntaxTree
44
module CLI
55
# A utility wrapper around colored strings in the output.
6-
class ColoredString
7-
COLORS = { default: "0", gray: "38;5;102", yellow: "33" }
6+
class Color
7+
attr_reader :value, :code
88

9-
attr_reader :code, :string
10-
11-
def initialize(color, string)
12-
@code = COLORS[color]
13-
@string = string
9+
def initialize(value, code)
10+
@value = value
11+
@code = code
1412
end
1513

1614
def to_s
17-
"\033[#{code}m#{string}\033[0m"
15+
"\033[#{code}m#{value}\033[0m"
16+
end
17+
18+
def self.gray(value)
19+
new(value, "38;5;102")
20+
end
21+
22+
def self.red(value)
23+
new(value, "1;31")
24+
end
25+
26+
def self.yellow(value)
27+
new(value, "33")
1828
end
1929
end
2030

@@ -37,23 +47,52 @@ def run(filepath, source)
3747
end
3848
end
3949

50+
# An action of the CLI that ensures that the filepath is formatted as
51+
# expected.
52+
class Check < Action
53+
class UnformattedError < StandardError
54+
end
55+
56+
def run(filepath, source)
57+
raise UnformattedError if source != SyntaxTree.format(source)
58+
rescue
59+
warn("[#{Color.yellow("warn")}] #{filepath}")
60+
raise
61+
end
62+
63+
def success
64+
puts("All files matched expected format.")
65+
end
66+
67+
def failure
68+
warn("The listed files did not match the expected format.")
69+
end
70+
end
71+
4072
# An action of the CLI that formats the source twice to check if the first
4173
# format is not idempotent.
42-
class Check < Action
74+
class Debug < Action
75+
class NonIdempotentFormatError < StandardError
76+
end
77+
4378
def run(filepath, source)
79+
warning = "[#{Color.yellow("warn")}] #{filepath}"
4480
formatted = SyntaxTree.format(source)
45-
return true if formatted == SyntaxTree.format(formatted)
4681

47-
puts "[#{ColoredString.new(:yellow, "warn")}] #{filepath}"
48-
false
82+
if formatted != SyntaxTree.format(formatted)
83+
raise NonIdempotentFormatError
84+
end
85+
rescue
86+
warn(warning)
87+
raise
4988
end
5089

5190
def success
52-
puts "All files matched expected format."
91+
puts("All files can be formatted idempotently.")
5392
end
5493

5594
def failure
56-
warn("The listed files did not match the expected format.")
95+
warn("The listed files could not be formatted idempotently.")
5796
end
5897
end
5998

@@ -83,10 +122,10 @@ def run(filepath, source)
83122
formatted = SyntaxTree.format(source)
84123
File.write(filepath, formatted)
85124

125+
color = source == formatted ? Color.gray(filepath) : filepath
86126
delta = ((Time.now - start) * 1000).round
87-
color = source == formatted ? :gray : :default
88127

89-
puts "\r#{ColoredString.new(color, filepath)} #{delta}ms"
128+
puts "\r#{color} #{delta}ms"
90129
end
91130
end
92131

@@ -95,7 +134,7 @@ def run(filepath, source)
95134
HELP = <<~HELP
96135
stree MODE FILE
97136
98-
MODE: ast | check | doc | format | write
137+
MODE: ast | check | debug | doc | format | write
99138
FILE: one or more paths to files to parse
100139
HELP
101140

@@ -115,7 +154,9 @@ def run(argv)
115154
AST.new
116155
when "c", "check"
117156
Check.new
118-
when "d", "doc"
157+
when "debug"
158+
Debug.new
159+
when "doc"
119160
Doc.new
120161
when "f", "format"
121162
Format.new
@@ -130,11 +171,37 @@ def run(argv)
130171
patterns.each do |pattern|
131172
Dir.glob(pattern).each do |filepath|
132173
next unless File.file?(filepath)
174+
source = source_for(filepath)
133175

134176
begin
135-
action.run(filepath, source_for(filepath))
177+
action.run(filepath, source)
178+
rescue ParseError => error
179+
warn("Error: #{error.message}")
180+
lines = source.lines
181+
182+
maximum = [error.lineno + 3, lines.length].min
183+
digits = Math.log10(maximum).ceil
184+
185+
([error.lineno - 3, 0].max...maximum).each do |line_index|
186+
line_number = line_index + 1
187+
188+
if line_number == error.lineno
189+
part1 = Color.red(">")
190+
part2 = Color.gray("%#{digits}d |" % line_number)
191+
warn("#{part1} #{part2} #{lines[line_index]}")
192+
193+
part3 = Color.gray(" %#{digits}s |" % " ")
194+
warn("#{part3} #{" " * error.column}#{Color.red("^")}")
195+
else
196+
prefix = Color.gray(" %#{digits}d |" % line_number)
197+
warn("#{prefix} #{lines[line_index]}")
198+
end
199+
end
200+
201+
errored = true
202+
rescue Check::UnformattedError, Debug::NonIdempotentFormatError
203+
errored = true
136204
rescue => error
137-
warn("!!! Failed on #{filepath}")
138205
warn(error.message)
139206
warn(error.backtrace)
140207
errored = true

lib/syntax_tree/prettyprint.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,9 +751,9 @@ def flush
751751
# This is a special sort used to order the line suffixes by both the
752752
# priority set on the line suffix and the index it was in the original
753753
# array.
754-
line_suffix_sort = ->(line_suffix) {
754+
line_suffix_sort = ->(line_suffix) do
755755
[-line_suffix.last, -line_suffixes.index(line_suffix)]
756-
}
756+
end
757757

758758
# This is a linear stack instead of a mutually recursive call defined on
759759
# the individual doc nodes for efficiency.

0 commit comments

Comments
 (0)