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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ceb99aac4328e67769eebf0c137e9e2f8412bbfc65c51c0757906f7e5e1ce88f
4
- data.tar.gz: 7bd420cb85700ace2a34e6e0460ee5f63f8a9b1530264f8b1f5d4a8f7f4583e3
3
+ metadata.gz: 583e74384e061c34bf2da93cd71156adc1b4d8c9d43dc86fec044364ff00f049
4
+ data.tar.gz: afa864b06807f97cf1e68d04128c77d930136982fccabcd91013893491b12f28
5
5
  SHA512:
6
- metadata.gz: a8225fdd2f9208f17b2feeb832d03ed7bbb0fd5669bfd875fccdff579d05050a846f229996ccdce028e028d47b48cc70f24355b7a9384409d99c77e31a0a4a62
7
- data.tar.gz: 0c5e2b51e492c0a56772ff6a9ed4309924a2da19e3a973c60782b1182605be59cec2cc3b10cc4c20a1641c21ae8102d9d37c3a6b00549fcf0e6a2b6347e84430
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 = failed_module_ids_for_created_paths(changed_paths)
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
- def failed_module_ids_for_created_paths(paths)
128
- relative_paths = relative_paths_for(paths)
129
- return [] if relative_paths.empty?
130
-
131
- records.filter_map do |module_id, record|
132
- next unless record.status == :failed
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
- def path_satisfies_unresolved_path?(path, unresolved_path)
148
- path == unresolved_path || path.start_with?("#{unresolved_path}.")
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)
@@ -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 "syntax_tree/haml"
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 pairs && error.message.start_with?("Invalid attribute list")
64
-
65
- raise ::Haml::SyntaxError.new("#{error.message}\n\n#{hash_attributes_hint(tag_prefix, pairs)}", error.line)
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