klenod-build 0.0.25 → 0.0.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 +4 -4
- data/lib/klenod/build/exception_formatter.rb +81 -0
- data/lib/klenod/build/graph/invalidator.rb +22 -21
- data/lib/klenod/build/graph.rb +15 -0
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +8 -3
- data/lib/klenod/build/resolution_error_formatter.rb +3 -14
- data/lib/klenod/build/source_error.rb +21 -9
- data/lib/klenod/build/source_excerpt.rb +135 -31
- data/lib/klenod/build/version.rb +1 -1
- data/lib/klenod/build.rb +2 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b1c963370596392fc4e48311db021c836c182e27665d8b62652bda1a55744e2
|
|
4
|
+
data.tar.gz: 454ee9440b137797885ee3b45d5fecc789fb64cc7adcf43e9a61eead08602299
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc351535e38a0599165dc4794cecfdc84064b5cf8b0273f7e3f763643a152d43391f413a2ab2d97845f0d3774c41989c925da95c4f2d12e32261569e963ec34c
|
|
7
|
+
data.tar.gz: 0e8b90fbaef71ccb01e419ac58e4f394cafe69c61d2926bac649802404a075cf89c1ee8df77b9cf9cc4acf6e958b3547bd9e338cc4ef6ba85e4d158d0ef2993d
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "klenod/runtime/backtrace_rewriter"
|
|
4
|
+
|
|
5
|
+
require_relative "source_excerpt"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
# A runtime exception as a development report, in the same shape as a
|
|
10
|
+
# SourceError: the class and message, an excerpt of the first frame that
|
|
11
|
+
# has source, then the frames.
|
|
12
|
+
#
|
|
13
|
+
# × NameError: undefined local variable or method 'x'
|
|
14
|
+
#
|
|
15
|
+
# ╭─[app:/routes/page.haml:4]
|
|
16
|
+
# 3 │ %h1 Hello
|
|
17
|
+
# > 4 │ %p= x
|
|
18
|
+
# ╰────
|
|
19
|
+
# at app:/routes/page.haml:4:in 'render'
|
|
20
|
+
# at /gems/rack/lib/rack.rb:12:in 'call'
|
|
21
|
+
#
|
|
22
|
+
# Rewriting lives in Klenod::Runtime::BacktraceRewriter, so a production
|
|
23
|
+
# application keeps corrected backtraces without loading any of this.
|
|
24
|
+
module ExceptionFormatter
|
|
25
|
+
Entry = Runtime::BacktraceRewriter::ParsedBacktraceEntry
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# `mods` maps module IDs to evaluated modules, as Graph#mods does. The
|
|
30
|
+
# error is rewritten in place.
|
|
31
|
+
def format(error, mods: {}, ansi: true, links: ansi && SourceExcerpt.hyperlinks?)
|
|
32
|
+
rewriter = Runtime::BacktraceRewriter.new(mods)
|
|
33
|
+
rewriter.rewrite_exception(error)
|
|
34
|
+
module_ids = mods.values.select { it.respond_to?(:eval_path) }.to_h { [it.eval_path.to_s, it.path.to_s] }
|
|
35
|
+
frames = Array(error.backtrace).map { Entry.parse(it) || it.to_s }
|
|
36
|
+
|
|
37
|
+
[
|
|
38
|
+
SourceExcerpt.header(error.class.name, error.message, ansi:),
|
|
39
|
+
[
|
|
40
|
+
excerpt(frames, rewriter, module_ids, ansi:, links:),
|
|
41
|
+
*frames.map { " #{frame(it, module_ids, ansi:, links:)}" }
|
|
42
|
+
].compact.join("\n")
|
|
43
|
+
].reject(&:empty?).join("\n\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The first frame the rewriter has original source for, boxed.
|
|
47
|
+
def excerpt(frames, rewriter, module_ids, ansi:, links:)
|
|
48
|
+
frames.grep(Entry).each do |entry|
|
|
49
|
+
source = rewriter.source_for(entry.file)
|
|
50
|
+
next unless source
|
|
51
|
+
|
|
52
|
+
location = location(entry, module_ids, ansi:, links:)
|
|
53
|
+
excerpt = SourceExcerpt.excerpt(source:, line: entry.line, ansi:, location:)
|
|
54
|
+
return excerpt if excerpt
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# "at app:/routes/page.haml:4:in 'render'". Frames outside the graph are
|
|
61
|
+
# dimmed so the application's own frames stand out.
|
|
62
|
+
def frame(entry, module_ids, ansi:, links:)
|
|
63
|
+
return SourceExcerpt.dim("at #{entry}", ansi:) unless entry.is_a?(Entry) && module_ids.key?(entry.file)
|
|
64
|
+
|
|
65
|
+
"#{SourceExcerpt.dim("at", ansi:)} #{location(entry, module_ids, ansi:, links:)}#{SourceExcerpt.dim(":in '#{entry.description}'", ansi:)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Graph modules are named by module ID and linked to the file they were
|
|
69
|
+
# evaluated from.
|
|
70
|
+
def location(entry, module_ids, ansi:, links:)
|
|
71
|
+
SourceExcerpt.styled_location(
|
|
72
|
+
module_id: module_ids.fetch(entry.file, entry.file),
|
|
73
|
+
line: entry.line,
|
|
74
|
+
path: entry.file,
|
|
75
|
+
ansi:,
|
|
76
|
+
links:
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -16,7 +16,8 @@ module Klenod
|
|
|
16
16
|
evaluated_module_ids = mods.keys
|
|
17
17
|
changed_module_ids = module_ids_for_paths(changed_paths)
|
|
18
18
|
removed_module_ids = module_ids_for_paths(removed_paths)
|
|
19
|
-
failed_retry_ids =
|
|
19
|
+
failed_retry_ids = failed_module_ids - removed_module_ids
|
|
20
|
+
imported_module_ids = records.each_key.select { |module_id| graph.dependents(module_id).any? }
|
|
20
21
|
pattern_owner_ids = module_ids_for_watched_paths(changed_paths + removed_paths)
|
|
21
22
|
plugin_owner_ids = plugin_invalidated_module_ids(changed_paths + removed_paths)
|
|
22
23
|
reload_module_ids = (changed_module_ids + failed_retry_ids + pattern_owner_ids + plugin_owner_ids).uniq
|
|
@@ -31,6 +32,12 @@ module Klenod
|
|
|
31
32
|
failed_reload_ids = []
|
|
32
33
|
reloaded_module_ids =
|
|
33
34
|
reload_module_ids.filter_map do |module_id|
|
|
35
|
+
if orphaned_failure?(module_id, imported_module_ids)
|
|
36
|
+
graph.remove_record(module_id)
|
|
37
|
+
mods.delete(module_id)
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
if evaluated_module_ids.include?(module_id)
|
|
35
42
|
graph.load_module(module_id, force: true)
|
|
36
43
|
else
|
|
@@ -124,28 +131,22 @@ module Klenod
|
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
unresolved_path = record.metadata.fetch(:error).unresolved_path if record.metadata.fetch(:error).respond_to?(:unresolved_path)
|
|
135
|
-
module_id if unresolved_path && relative_paths.any? { |path| path_satisfies_unresolved_path?(path, unresolved_path) }
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def relative_paths_for(paths)
|
|
140
|
-
paths.filter_map do |path|
|
|
141
|
-
Pathname.new(path).expand_path.relative_path_from(resolver.source_dir).to_s
|
|
142
|
-
rescue ArgumentError
|
|
143
|
-
nil
|
|
144
|
-
end
|
|
134
|
+
# A failed record keeps no dependency links, so nothing ties it to the file
|
|
135
|
+
# that broke it: a missing import, or a new dependency that fails to parse.
|
|
136
|
+
# Retry every failed module on each change. Failures are rare, and a retry
|
|
137
|
+
# that fails again reports its error again.
|
|
138
|
+
def failed_module_ids
|
|
139
|
+
records.filter_map { |module_id, record| module_id if record.status == :failed }
|
|
145
140
|
end
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
# A failed module whose last importer dropped it earlier in this update.
|
|
143
|
+
# Nothing can demand it any more, so retrying it would report its error
|
|
144
|
+
# on every change. Its record goes; importing it again collects it anew.
|
|
145
|
+
# A failed entry never had an importer and keeps being retried.
|
|
146
|
+
def orphaned_failure?(module_id, imported_module_ids)
|
|
147
|
+
records[module_id]&.status == :failed &&
|
|
148
|
+
imported_module_ids.include?(module_id) &&
|
|
149
|
+
graph.dependents(module_id).empty?
|
|
149
150
|
end
|
|
150
151
|
|
|
151
152
|
def plugin_invalidated_module_ids(paths)
|
data/lib/klenod/build/graph.rb
CHANGED
|
@@ -386,6 +386,18 @@ module Klenod
|
|
|
386
386
|
@mods[module_id] = mod
|
|
387
387
|
record
|
|
388
388
|
end
|
|
389
|
+
rescue SourceError => error
|
|
390
|
+
error.path ||= source_path_for(error.module_id)
|
|
391
|
+
raise
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# The file behind an app module, so error reports can link to it.
|
|
395
|
+
def source_path_for(module_id)
|
|
396
|
+
return nil unless module_id.is_a?(ModuleId) && module_id.scheme == :app
|
|
397
|
+
|
|
398
|
+
absolute_path(module_id).to_s
|
|
399
|
+
rescue ResolveError
|
|
400
|
+
nil
|
|
389
401
|
end
|
|
390
402
|
|
|
391
403
|
def collect_module(module_id, force: false)
|
|
@@ -443,6 +455,9 @@ module Klenod
|
|
|
443
455
|
@profiler.progress(:collect_module, module_id: record.id.to_s, total_records: @records.length)
|
|
444
456
|
record
|
|
445
457
|
end
|
|
458
|
+
rescue SourceError => error
|
|
459
|
+
error.path ||= source_path_for(error.module_id)
|
|
460
|
+
raise
|
|
446
461
|
end
|
|
447
462
|
|
|
448
463
|
def evaluate_module(module_id)
|
|
@@ -60,9 +60,14 @@ module Klenod
|
|
|
60
60
|
begin
|
|
61
61
|
attributes, rest, last_line = super
|
|
62
62
|
rescue ::Haml::SyntaxError => error
|
|
63
|
-
raise unless
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
raise unless error.message.start_with?("Invalid attribute list")
|
|
64
|
+
|
|
65
|
+
# Haml quotes the list with String#inspect, which escapes every
|
|
66
|
+
# `"` in it. Show the source as written instead.
|
|
67
|
+
attribute_list = ::Haml::Util.balance(text, "(", ")")&.first || text
|
|
68
|
+
message = "Invalid attribute list: `#{attribute_list}`."
|
|
69
|
+
message = "#{message}\n\n#{hash_attributes_hint(tag_prefix, pairs)}" if pairs
|
|
70
|
+
raise ::Haml::SyntaxError.new(message, error.line)
|
|
66
71
|
end
|
|
67
72
|
return [attributes, rest, last_line + joined_lines]
|
|
68
73
|
end
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "errors"
|
|
4
|
+
require_relative "source_excerpt"
|
|
4
5
|
|
|
5
6
|
module Klenod
|
|
6
7
|
module Build
|
|
7
8
|
module ResolutionErrorFormatter
|
|
8
|
-
ANSI_BACKGROUND = "\e[0;48;5;52m"
|
|
9
|
-
|
|
10
9
|
module_function
|
|
11
10
|
|
|
12
11
|
def format(error, source_root: nil, source_context: nil, ansi: false)
|
|
13
12
|
return error.message unless error.resolution_failure?
|
|
14
13
|
|
|
15
|
-
lines = [error.title, "", " Import: #{error.requested_specifier}"]
|
|
14
|
+
lines = [SourceExcerpt.header(error.title, "", ansi:), "", " Import: #{error.requested_specifier}"]
|
|
16
15
|
lines << " Imported by: #{error.imported_by}" if error.imported_by
|
|
17
16
|
lines << " Source root: #{source_root}" if source_root
|
|
18
17
|
append_suggestions(lines, error)
|
|
19
18
|
lines.concat(["", source_context]) if source_context
|
|
20
|
-
|
|
21
|
-
ansi ? format_ansi(text) : text
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def format_ansi(text)
|
|
25
|
-
title, separator, body = text.partition("\n")
|
|
26
|
-
body = body.gsub("\e[0m", ANSI_BACKGROUND)
|
|
27
|
-
body = "\n#{body}" unless separator.empty?
|
|
28
|
-
|
|
29
|
-
"\e[1;31;47m ERROR \e[1;37;41m #{title} #{ANSI_BACKGROUND}#{body}\e[0m"
|
|
19
|
+
lines.join("\n")
|
|
30
20
|
end
|
|
31
|
-
private_class_method :format_ansi
|
|
32
21
|
|
|
33
22
|
def append_suggestions(lines, error)
|
|
34
23
|
return if error.suggestions.empty?
|
|
@@ -27,7 +27,7 @@ module Klenod
|
|
|
27
27
|
def initialize(line: nil, column: nil, detail: nil, hints: []) = super
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
attr_reader :module_id, :source, :line, :column, :cause, :detail, :hints
|
|
30
|
+
attr_reader :module_id, :source, :line, :column, :cause, :detail, :hints, :path
|
|
31
31
|
|
|
32
32
|
def initialize(error, source:, module_id:)
|
|
33
33
|
@cause = error
|
|
@@ -40,19 +40,31 @@ module Klenod
|
|
|
40
40
|
@detail = found.detail || message_for(error)
|
|
41
41
|
@hints = Array(found.hints).map(&:to_s).reject(&:empty?).freeze
|
|
42
42
|
|
|
43
|
-
super(
|
|
43
|
+
super(@detail)
|
|
44
|
+
|
|
45
|
+
set_backtrace(backtrace_for(error))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The file the module was read from, when it has one. The graph sets it
|
|
49
|
+
# as the error leaves the module, since plugins only know the module ID,
|
|
50
|
+
# and the message links the location to it.
|
|
51
|
+
def path=(path)
|
|
52
|
+
@path = path
|
|
53
|
+
@message = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_s
|
|
57
|
+
@message ||=
|
|
44
58
|
SourceExcerpt.message(
|
|
45
59
|
module_id:,
|
|
46
|
-
line
|
|
47
|
-
column
|
|
60
|
+
line:,
|
|
61
|
+
column:,
|
|
48
62
|
kind:,
|
|
49
63
|
source:,
|
|
50
|
-
message:
|
|
51
|
-
hints
|
|
64
|
+
message: detail,
|
|
65
|
+
hints:,
|
|
66
|
+
path:
|
|
52
67
|
)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
set_backtrace(backtrace_for(error))
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# "JavaScript parse error". Used as the label in the browser error dialog,
|
|
@@ -10,34 +10,120 @@ module Klenod
|
|
|
10
10
|
# `ansi: false` to get the same layout without escape codes.
|
|
11
11
|
module SourceExcerpt
|
|
12
12
|
MARKED_LINE = "\e[1;31m"
|
|
13
|
+
KIND = "\e[1;31m"
|
|
14
|
+
DETAIL = "\e[31m"
|
|
15
|
+
HINT_LABEL = "\e[1;36m"
|
|
16
|
+
HINT = "\e[36m"
|
|
17
|
+
LOCATION = "\e[34m"
|
|
18
|
+
BOLD = "\e[1m"
|
|
19
|
+
NOT_BOLD = "\e[22m"
|
|
20
|
+
DIM = "\e[2m"
|
|
13
21
|
RESET = "\e[0m"
|
|
14
22
|
|
|
15
23
|
module_function
|
|
16
24
|
|
|
17
|
-
# "app:/pages/thing.tsx:33:9
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# "app:/pages/thing.tsx:33:9"
|
|
26
|
+
def location(module_id:, line:, column: nil)
|
|
27
|
+
if module_id && line
|
|
28
|
+
"#{module_id}:#{[line, column].compact.join(":")}"
|
|
29
|
+
elsif module_id
|
|
30
|
+
module_id.to_s
|
|
31
|
+
elsif line
|
|
32
|
+
column ? "line #{line} column #{column}" : "line #{line}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
# The kind and detail share a header line. The location goes in the
|
|
37
|
+
# excerpt's frame, or on an "at" line when there is no excerpt, and the
|
|
38
|
+
# hints follow:
|
|
39
|
+
#
|
|
40
|
+
# × Haml parse error: Could not parse Ruby filter
|
|
41
|
+
#
|
|
42
|
+
# ╭─[app:/pages/page.haml:4]
|
|
43
|
+
# 3 │ @count = 0
|
|
44
|
+
# > 4 │
|
|
45
|
+
# ╰────
|
|
46
|
+
# hint: Unmatched keyword, missing `end' ?
|
|
47
|
+
#
|
|
48
|
+
# With `path`, the file the module was read from, the location links to
|
|
49
|
+
# that file when `links` is on.
|
|
50
|
+
def message(module_id:, line:, kind:, source:, message:, column: nil, hints: [], context: 2, ansi: true, path: nil, links: ansi && hyperlinks?)
|
|
51
|
+
location = styled_location(module_id:, line:, column:, path:, ansi:, links:)
|
|
52
|
+
excerpt = excerpt(source:, line:, column:, context:, ansi:, location:)
|
|
53
|
+
hints = hint_section(hints, ansi:)&.gsub(/^/, " ")
|
|
54
|
+
header = header(kind, message, ansi:)
|
|
55
|
+
|
|
56
|
+
unless excerpt
|
|
57
|
+
header = "#{header}\n #{dim("at", ansi:)} #{location}" if location
|
|
58
|
+
return [header, hints].compact.join("\n\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[header, [excerpt, hints].compact.join("\n")].join("\n\n")
|
|
29
62
|
end
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
# "× JSON parse error: expected object key". Later lines of a multi-line
|
|
65
|
+
# detail are indented under the first.
|
|
66
|
+
def header(kind, message, ansi: true)
|
|
67
|
+
first, *rest = message.to_s.lines(chomp: true)
|
|
68
|
+
label = first ? "× #{kind}:" : "× #{kind}"
|
|
69
|
+
label = "#{KIND}#{label}#{RESET}" if ansi
|
|
70
|
+
return label unless first
|
|
71
|
+
|
|
32
72
|
[
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
"#{label} #{detail(first, ansi:)}",
|
|
74
|
+
*rest.map { it.empty? ? it : " #{detail(it, ansi:)}" }
|
|
75
|
+
].join("\n")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The location in blue with the file name in bold, linked to `path` when
|
|
79
|
+
# `links` is on and the file exists.
|
|
80
|
+
def styled_location(module_id:, line: nil, column: nil, path: nil, ansi: true, links: ansi && hyperlinks?)
|
|
81
|
+
location = location(module_id:, line:, column:)
|
|
82
|
+
return location unless ansi && location
|
|
83
|
+
|
|
84
|
+
location = bold_file_name(location, module_id) if module_id
|
|
85
|
+
location = file_link(location, path:, line:, column:) if links && path && File.file?(path.to_s)
|
|
86
|
+
"#{LOCATION}#{location}#{RESET}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# "app:\e[1m/pages/thing.tsx\e[22m:33:9": the file name stands out from its
|
|
90
|
+
# scheme and position.
|
|
91
|
+
def bold_file_name(location, module_id)
|
|
92
|
+
scheme, name = module_id.to_s.match(/\A([a-z][a-z0-9+.-]*:)?(.*)\z/i).captures
|
|
93
|
+
location.sub(module_id.to_s) { "#{scheme}#{BOLD}#{name}#{NOT_BOLD}" }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether the terminal turns OSC 8 sequences into clickable links. There
|
|
97
|
+
# is no way to ask, so this recognises the terminals known to do it.
|
|
98
|
+
def hyperlinks?(env = ENV)
|
|
99
|
+
return false if env.key?("NO_COLOR")
|
|
100
|
+
return true if %w[iTerm.app WezTerm vscode ghostty Hyper Tabby rio].include?(env["TERM_PROGRAM"])
|
|
101
|
+
return true if %w[xterm-kitty foot contour].include?(env["TERM"])
|
|
102
|
+
return true if env.key?("KITTY_WINDOW_ID") || env.key?("WT_SESSION") || env.key?("ALACRITTY_WINDOW_ID")
|
|
103
|
+
return true if env["VTE_VERSION"].to_i >= 5000
|
|
104
|
+
return true if env["KONSOLE_VERSION"].to_i >= 201200
|
|
105
|
+
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Wraps `text` in an OSC 8 hyperlink to `path`, with the line and column
|
|
110
|
+
# as the fragment: "file:///app/pages/thing.tsx#33:9".
|
|
111
|
+
def file_link(text, path:, line: nil, column: nil)
|
|
112
|
+
encoded = path.to_s.b.gsub(%r{[^A-Za-z0-9\-._~/]}) { format("%%%02X", it.ord) }
|
|
113
|
+
fragment = line && "##{[line, column].compact.join(":")}"
|
|
114
|
+
|
|
115
|
+
"\e]8;;file://#{encoded}#{fragment}\e\\#{text}\e]8;;\e\\"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Colored per line so loggers that prefix each line do not carry the
|
|
119
|
+
# color into their gutter.
|
|
120
|
+
def detail(message, ansi: true)
|
|
121
|
+
return message unless ansi && message
|
|
122
|
+
|
|
123
|
+
message.to_s.lines(chomp: true).map { it.empty? ? it : "#{DETAIL}#{it}#{RESET}" }.join("\n")
|
|
38
124
|
end
|
|
39
125
|
|
|
40
|
-
def excerpt(source:, line:, column: nil, context: 2, ansi: true)
|
|
126
|
+
def excerpt(source:, line:, column: nil, context: 2, ansi: true, location: nil)
|
|
41
127
|
return nil unless line
|
|
42
128
|
|
|
43
129
|
lines = source.to_s.lines
|
|
@@ -49,35 +135,53 @@ module Klenod
|
|
|
49
135
|
first = [index - context, 0].max
|
|
50
136
|
last = [index + context, lines.length - 1].min
|
|
51
137
|
width = (last + 1).to_s.length
|
|
138
|
+
gutter = " " * (width + 3)
|
|
52
139
|
|
|
53
140
|
rows =
|
|
54
141
|
(first..last).flat_map do |line_index|
|
|
55
142
|
marked = line_index == index
|
|
56
143
|
number = (line_index + 1).to_s.rjust(width)
|
|
57
|
-
|
|
58
|
-
formatted =
|
|
59
|
-
|
|
60
|
-
|
|
144
|
+
code = lines.fetch(line_index).chomp
|
|
145
|
+
formatted =
|
|
146
|
+
if marked
|
|
147
|
+
row = "> #{number} │ #{code}".rstrip
|
|
148
|
+
ansi ? "#{MARKED_LINE}#{row}#{RESET}" : row
|
|
149
|
+
else
|
|
150
|
+
"#{dim(" #{number} │", ansi:)} #{code}".rstrip
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
marked ? [formatted, caret_row(width, column, ansi:)].compact : [formatted]
|
|
61
154
|
end
|
|
62
155
|
|
|
63
|
-
|
|
156
|
+
[
|
|
157
|
+
location ? "#{gutter}#{dim("╭─[", ansi:)}#{location}#{dim("]", ansi:)}" : "#{gutter}#{dim("╭────", ansi:)}",
|
|
158
|
+
*rows,
|
|
159
|
+
"#{gutter}#{dim("╰────", ansi:)}"
|
|
160
|
+
].join("\n")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# `text` without color codes or OSC 8 links, as a terminal would show it.
|
|
164
|
+
def strip(text)
|
|
165
|
+
text.to_s.gsub(/\e\[[0-9;]*m|\e\]8;[^\e\a]*(?:\e\\|\a)/, "")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def dim(text, ansi: true)
|
|
169
|
+
ansi ? "#{DIM}#{text}#{RESET}" : text
|
|
64
170
|
end
|
|
65
171
|
|
|
66
|
-
# "
|
|
67
|
-
def caret_row(width, column)
|
|
172
|
+
# " · ^", aligned under the offending column of the marked line.
|
|
173
|
+
def caret_row(width, column, ansi: true)
|
|
68
174
|
return nil unless column&.positive?
|
|
69
175
|
|
|
70
|
-
"#{" " * (width + 3)}
|
|
176
|
+
"#{" " * (width + 3)}#{dim("·", ansi:)} #{" " * (column - 1)}^"
|
|
71
177
|
end
|
|
72
178
|
|
|
73
|
-
# What to try next
|
|
74
|
-
|
|
75
|
-
def hint_section(hints)
|
|
179
|
+
# What to try next, one "hint: ..." line per hint.
|
|
180
|
+
def hint_section(hints, ansi: true)
|
|
76
181
|
hints = Array(hints).map(&:to_s).reject(&:empty?)
|
|
77
182
|
return nil if hints.empty?
|
|
78
183
|
|
|
79
|
-
|
|
80
|
-
"#{heading}\n#{hints.map { " #{it}" }.join("\n")}"
|
|
184
|
+
hints.map { ansi ? "#{HINT_LABEL}hint:#{RESET} #{HINT}#{it}#{RESET}" : "hint: #{it}" }.join("\n")
|
|
81
185
|
end
|
|
82
186
|
end
|
|
83
187
|
end
|
data/lib/klenod/build/version.rb
CHANGED
data/lib/klenod/build.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "build/version"
|
|
4
4
|
require "klenod/runtime"
|
|
5
5
|
require_relative "build/context"
|
|
6
|
+
require_relative "build/exception_formatter"
|
|
7
|
+
require_relative "build/resolution_error_formatter"
|
|
6
8
|
|
|
7
9
|
module Klenod
|
|
8
10
|
class Error < StandardError; end unless const_defined?(:Error, false)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klenod-build
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrés Alin
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.26
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.26
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -257,6 +257,7 @@ files:
|
|
|
257
257
|
- lib/klenod/build/context.rb
|
|
258
258
|
- lib/klenod/build/dependency.rb
|
|
259
259
|
- lib/klenod/build/errors.rb
|
|
260
|
+
- lib/klenod/build/exception_formatter.rb
|
|
260
261
|
- lib/klenod/build/filesystem_resolver.rb
|
|
261
262
|
- lib/klenod/build/graph.rb
|
|
262
263
|
- lib/klenod/build/graph/invalidator.rb
|