klenod-build 0.0.25 → 0.0.27
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 +9 -4
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +117 -430
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +20 -28
- data/lib/klenod/build/plugins/haml_plugin.rb +6 -6
- data/lib/klenod/build/plugins/ruby_plugin.rb +9 -21
- data/lib/klenod/build/resolution_error_formatter.rb +3 -14
- data/lib/klenod/build/ruby_import_rewriter.rb +94 -94
- 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 +30 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 583e74384e061c34bf2da93cd71156adc1b4d8c9d43dc86fec044364ff00f049
|
|
4
|
+
data.tar.gz: afa864b06807f97cf1e68d04128c77d930136982fccabcd91013893491b12f28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d33327a02fc995e93eb644b558dcee29fe32dd1223f6dc2a50fa47e922d7bddb50f81065bc4df3f95297e43cc9e4aa7b89336f723edc762e7531457956a180e
|
|
7
|
+
data.tar.gz: 7e29a8f5aaacef578ca6efa7c2e10e0595afee4058d8a58726899f2d97f28c12b7c5376931a6d0ab993b4c11b6ea8c4bb065c0a4abbab18df51ebf14069a27a0
|
|
@@ -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)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ripper"
|
|
4
|
-
require "
|
|
4
|
+
require "haml"
|
|
5
5
|
|
|
6
6
|
module Klenod
|
|
7
7
|
module Build
|
|
@@ -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
|