rigortype 0.4.2 → 0.4.3
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/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../hashing/xxh3"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Lens
|
|
7
|
+
# A site's anchor and contract-comment span (docs/internal-spec/lens.md § Anchors). The hashed material is the
|
|
8
|
+
# contract comment lines above the declaration (`#:`, `@rbs`, `@extrbs` and their continuation lines), the
|
|
9
|
+
# declaration's own bytes, and a `#:` comment trailing its last line. Documentation prose is not part of it.
|
|
10
|
+
class Anchor
|
|
11
|
+
Result = Data.define(:token, :line, :contract_comments)
|
|
12
|
+
|
|
13
|
+
# rbs-inline's annotation grammar (`#:`, `#[`, `#\s*@rbs`) plus `@extrbs`, matched from the `#`.
|
|
14
|
+
CONTRACT_COMMENT = /\A#(?::|\[|\s*@rbs\b|\s*@extrbs\b)/
|
|
15
|
+
# A `#:nodoc:`-style RDoc directive, which the rbs-inline plugin neutralises rather than reads.
|
|
16
|
+
RDOC_DIRECTIVE = /\A#:[a-z_][\w-]*:/
|
|
17
|
+
# A kept line that later comment lines may continue, with the spaces between its `#` and the tag.
|
|
18
|
+
CONTINUED_TAG = /\A#( *)@(?:rbs|extrbs)\b/
|
|
19
|
+
|
|
20
|
+
# @param source — the file's bytes (binary String)
|
|
21
|
+
# @param comments — the file's `Prism::Comment` list
|
|
22
|
+
def initialize(source, comments)
|
|
23
|
+
@source = source.b
|
|
24
|
+
@line_starts = [0]
|
|
25
|
+
@source.each_byte.with_index { |byte, index| @line_starts << (index + 1) if byte == 10 }
|
|
26
|
+
@comment_by_line = {}
|
|
27
|
+
comments.each do |comment|
|
|
28
|
+
next unless comment.is_a?(Prism::InlineComment)
|
|
29
|
+
|
|
30
|
+
@comment_by_line[comment.location.start_line] = comment
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The anchor of the declaration spanning bytes `start_offset...end_offset` and lines `start_line..end_line`.
|
|
35
|
+
def for_span(start_offset, end_offset, start_line, end_line)
|
|
36
|
+
leading = leading_contract_comments(start_line)
|
|
37
|
+
trailing = trailing_contract_comment(end_line, end_offset)
|
|
38
|
+
material = +"".b
|
|
39
|
+
leading.each { |comment| material << line_text_from(comment.location.start_offset) << "\n" }
|
|
40
|
+
material << @source.byteslice(start_offset, end_offset - start_offset)
|
|
41
|
+
material << "\n" << line_text_from(trailing.location.start_offset) if trailing
|
|
42
|
+
line = leading.empty? ? start_line : leading.first.location.start_line
|
|
43
|
+
Result.new(token: "#{line}:#{Hashing::XXH3.anchor_hash(material)}", line: line,
|
|
44
|
+
contract_comments: leading + [trailing].compact)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# The contract comments among the contiguous comment-only lines directly above `start_line`, in file order,
|
|
50
|
+
# with rbs-inline's continuation lines below a kept `@rbs` / `@extrbs` line.
|
|
51
|
+
def leading_contract_comments(start_line)
|
|
52
|
+
run = []
|
|
53
|
+
line = start_line - 1
|
|
54
|
+
while line >= 1 && (comment = comment_only_line(line))
|
|
55
|
+
run.unshift(comment)
|
|
56
|
+
line -= 1
|
|
57
|
+
end
|
|
58
|
+
kept = []
|
|
59
|
+
tag_indent = nil
|
|
60
|
+
run.each do |comment|
|
|
61
|
+
text = comment_text(comment)
|
|
62
|
+
if tag_indent && hash_indent(text) > tag_indent
|
|
63
|
+
kept << comment
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
tag_indent = nil
|
|
68
|
+
next unless contract_comment?(text)
|
|
69
|
+
|
|
70
|
+
kept << comment
|
|
71
|
+
continued = text.match(CONTINUED_TAG)
|
|
72
|
+
tag_indent = continued && continued[1].size
|
|
73
|
+
end
|
|
74
|
+
kept
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def trailing_contract_comment(end_line, end_offset)
|
|
78
|
+
comment = @comment_by_line[end_line]
|
|
79
|
+
return nil if comment.nil? || comment.location.start_offset < end_offset
|
|
80
|
+
|
|
81
|
+
text = comment_text(comment)
|
|
82
|
+
contract_comment?(text) && text.start_with?("#:") ? comment : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def contract_comment?(text)
|
|
86
|
+
text.match?(CONTRACT_COMMENT) && !text.match?(RDOC_DIRECTIVE)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The spaces between a comment's `#` and its first non-space byte.
|
|
90
|
+
def hash_indent(text)
|
|
91
|
+
text[/\A#( *)/, 1].size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The comment on `line` when nothing but whitespace precedes it.
|
|
95
|
+
def comment_only_line(line)
|
|
96
|
+
comment = @comment_by_line[line]
|
|
97
|
+
return nil if comment.nil?
|
|
98
|
+
|
|
99
|
+
prefix = @source.byteslice(@line_starts[line - 1], comment.location.start_offset - @line_starts[line - 1])
|
|
100
|
+
prefix.match?(/\A[ \t]*\z/) ? comment : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The comment's text from its `#`, without a line terminator.
|
|
104
|
+
def comment_text(comment)
|
|
105
|
+
comment.location.slice.b.chomp.delete_suffix("\r")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The bytes from `offset` to the end of its line, without the line terminator.
|
|
109
|
+
def line_text_from(offset)
|
|
110
|
+
line_end = @source.index("\n", offset) || @source.bytesize
|
|
111
|
+
@source.byteslice(offset, line_end - offset).delete_suffix("\r")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "slot"
|
|
4
|
+
require_relative "../reflection"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Lens
|
|
8
|
+
# Reads a member's own RBS declaration into overloads with per-slot provenance (docs/internal-spec/lens.md
|
|
9
|
+
# § Members). A declaration the member inherits is not its contract; one written for its own class is, whether
|
|
10
|
+
# it comes from `sig/`, an inline comment, a plugin or a library.
|
|
11
|
+
class ContractReader
|
|
12
|
+
INFERRED_RETURN = "rigor:v1:inferred-return"
|
|
13
|
+
INFERRED_SIGNATURE = "rigor:v1:inferred-signature"
|
|
14
|
+
|
|
15
|
+
# `overloads` hold `{rbs, params, block, return}`; a return the contract leaves to inference is nil there,
|
|
16
|
+
# with `return_inferred` set. `buffers` are the declaring buffer names.
|
|
17
|
+
Contract = Data.define(:overloads, :provenance, :path, :return_inferred, :buffers)
|
|
18
|
+
|
|
19
|
+
def initialize(provenance)
|
|
20
|
+
@provenance = provenance
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The member's contract, or nil when it has none of its own. `name` is the Ruby method name.
|
|
24
|
+
def read(owner, scope, name, lookup_scope)
|
|
25
|
+
method = lookup(owner, scope, name, lookup_scope)
|
|
26
|
+
defs = method ? method.defs.select { |definition| definition.defined_in.to_s.delete_prefix("::") == owner } : []
|
|
27
|
+
return nil if defs.empty?
|
|
28
|
+
|
|
29
|
+
annotations = annotations_of(method, defs)
|
|
30
|
+
return nil if annotations.include?(INFERRED_SIGNATURE)
|
|
31
|
+
|
|
32
|
+
contract(defs, annotations)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
unreadable("definition_build_failed", [])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def contract(defs, annotations)
|
|
40
|
+
buffers = defs.map { |definition| buffer_name(definition) }
|
|
41
|
+
classified = buffers.map { |buffer| @provenance.classify(buffer) }
|
|
42
|
+
return unreadable("not_computed", buffers) if classified.include?(nil)
|
|
43
|
+
|
|
44
|
+
return_inferred = annotations.include?(INFERRED_RETURN)
|
|
45
|
+
authored = annotations.select do |string|
|
|
46
|
+
string.start_with?("rigor:v1:") && !string.start_with?(INFERRED_RETURN)
|
|
47
|
+
end
|
|
48
|
+
# Each overload carries the provenance of the declaration it came from: a `sig/` overload extending a
|
|
49
|
+
# library declaration reads `sig`, the library one `library`.
|
|
50
|
+
overloads = defs.zip(classified).map do |definition, (provenance, path)|
|
|
51
|
+
overload(definition.type, provenance, path, return_inferred, authored)
|
|
52
|
+
end
|
|
53
|
+
provenance, path = classified.first
|
|
54
|
+
Contract.new(overloads: overloads, provenance: provenance, path: path,
|
|
55
|
+
return_inferred: return_inferred, buffers: buffers)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The method's and its own declarations' annotation strings: the plugin's defaulted-slot marks and authored
|
|
59
|
+
# `%a{…}`.
|
|
60
|
+
def annotations_of(method, defs)
|
|
61
|
+
strings = method.annotations.map(&:string) + defs.flat_map { |definition| definition.annotations.map(&:string) }
|
|
62
|
+
strings.map(&:strip).uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def buffer_name(definition)
|
|
66
|
+
location = definition.member&.location
|
|
67
|
+
location ? location.buffer&.name.to_s : ""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lookup(owner, scope, name, lookup_scope)
|
|
71
|
+
if scope == :singleton
|
|
72
|
+
Reflection.singleton_method_definition(owner, name.to_sym, scope: lookup_scope)
|
|
73
|
+
else
|
|
74
|
+
Reflection.instance_method_definition(owner, name.to_sym, scope: lookup_scope)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A declaration Rigor cannot read: its buffer name did not survive the environment cache
|
|
79
|
+
# (`not_computed`), or building the member's definition failed (`definition_build_failed`). The member has a
|
|
80
|
+
# contract, so it is not inferred; every slot is unavailable, and so are its parameters (`params: null`).
|
|
81
|
+
def unreadable(reason, buffers)
|
|
82
|
+
Contract.new(overloads: [{ "rbs" => nil, "params" => nil, "block" => nil,
|
|
83
|
+
"return" => Slot.unavailable(reason) }],
|
|
84
|
+
provenance: nil, path: nil, return_inferred: false, buffers: buffers)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def overload(method_type, provenance, path, return_inferred, annotations)
|
|
88
|
+
function = method_type.type
|
|
89
|
+
params = function.respond_to?(:required_positionals) ? params_of(function, provenance, path) : nil
|
|
90
|
+
block = method_type.block && { "required" => method_type.block.required,
|
|
91
|
+
"type" => Slot.from_rbs(method_type.block.type.to_s, provenance, path) }
|
|
92
|
+
prefix = annotations.map { |string| "%a{#{string}} " }.join
|
|
93
|
+
return_slot = return_inferred ? nil : Slot.from_rbs(function.return_type.to_s, provenance, path)
|
|
94
|
+
{ "rbs" => "#{prefix}#{method_type}", "params" => params, "block" => block, "return" => return_slot,
|
|
95
|
+
"return_rbs" => function.return_type.to_s }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def params_of(function, provenance, path)
|
|
99
|
+
(positional_params(function) + keyword_params(function)).map do |kind, name, param|
|
|
100
|
+
{ "name" => name&.to_s, "kind" => kind, "type" => Slot.from_rbs(param.type.to_s, provenance, path) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def positional_params(function)
|
|
105
|
+
function.required_positionals.map { |param| ["required", param.name, param] } +
|
|
106
|
+
function.optional_positionals.map { |param| ["optional", param.name, param] } +
|
|
107
|
+
[function.rest_positionals].compact.map { |param| ["rest", param.name, param] } +
|
|
108
|
+
function.trailing_positionals.map { |param| ["post", param.name, param] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def keyword_params(function)
|
|
112
|
+
function.required_keywords.map { |name, param| ["keyreq", name, param] } +
|
|
113
|
+
function.optional_keywords.map { |name, param| ["key", name, param] } +
|
|
114
|
+
[function.rest_keywords].compact.map { |param| ["keyrest", param.name, param] }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "../version"
|
|
6
|
+
require_relative "../cache/engine_source"
|
|
7
|
+
require_relative "../analysis/run_cache_key"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Lens
|
|
11
|
+
# Assembles the `lens/v0` document (docs/internal-spec/lens.md § Document) from a {Run::Outcome}.
|
|
12
|
+
class Document
|
|
13
|
+
SITE_FIELDS = %w[key kind name owner scope attribute members ordinal span contract_span anchor visibility
|
|
14
|
+
contract params block body_return].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(outcome:, configuration:, root:, config_path:)
|
|
17
|
+
@outcome = outcome
|
|
18
|
+
@configuration = configuration
|
|
19
|
+
@root = root
|
|
20
|
+
@config_path = config_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{ "schema" => SCHEMA, "revision" => REVISION, "producer" => producer, "context" => context,
|
|
25
|
+
"capabilities" => CAPABILITIES, "files" => @outcome.targets.map { |target| file(target) },
|
|
26
|
+
"members" => @outcome.member_rows }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def producer
|
|
32
|
+
identity = begin
|
|
33
|
+
Cache::EngineSource.process_identity
|
|
34
|
+
rescue StandardError
|
|
35
|
+
"unknown"
|
|
36
|
+
end
|
|
37
|
+
{ "name" => "rigor", "version" => Rigor::VERSION, "engine_source" => identity }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def context
|
|
41
|
+
fields = {
|
|
42
|
+
"config" => @config_path && relative(File.expand_path(@config_path)),
|
|
43
|
+
"target_ruby" => @configuration.target_ruby,
|
|
44
|
+
"rbs_version" => rbs_version,
|
|
45
|
+
"signature_roots" => signature_roots.map { |root| relative(root) },
|
|
46
|
+
"signature_digest" => signature_digest,
|
|
47
|
+
"plugins" => @outcome.plugins,
|
|
48
|
+
"lockfiles" => lockfiles
|
|
49
|
+
}
|
|
50
|
+
fields.merge("digest" => Lens.digest(JSON.generate(fields)))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rbs_version
|
|
54
|
+
require "rbs"
|
|
55
|
+
RBS::VERSION
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def signature_roots
|
|
59
|
+
Environment.project_signature_roots(@configuration.signature_paths).map { |path| File.expand_path(path.to_s) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def signature_digest
|
|
63
|
+
listing = signature_roots.flat_map { |root| Dir.glob(File.join(root, "**", "*.rbs")) }.sort.map do |path|
|
|
64
|
+
"#{relative(path)}\t#{Lens.digest(File.binread(path))}\n"
|
|
65
|
+
end
|
|
66
|
+
Lens.digest(listing.join)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def lockfiles
|
|
70
|
+
Analysis::RunCacheKey.resolved_lockfile_paths(@configuration).compact.filter_map do |path|
|
|
71
|
+
absolute = File.expand_path(path.to_s)
|
|
72
|
+
next unless File.file?(absolute)
|
|
73
|
+
|
|
74
|
+
{ "path" => relative(absolute), "digest" => Lens.digest(File.binread(absolute)) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Every field is present; those that apply only to some statuses are null elsewhere (lens.md § Files).
|
|
79
|
+
def file(target)
|
|
80
|
+
read = %w[ok parse_error].include?(target.status)
|
|
81
|
+
head = { "argument" => target.argument, "path" => target.status == "not_found" ? nil : target.path,
|
|
82
|
+
"status" => target.status, "reason" => target.status == "ok" ? nil : target.reason,
|
|
83
|
+
"digest" => file_digest(target, read) }
|
|
84
|
+
head.merge(analysed_fields(target.status == "ok" ? target : nil))
|
|
85
|
+
.merge("diagnostics" => read ? target.diagnostics.map { |diagnostic| diagnostic_row(diagnostic) } : nil)
|
|
86
|
+
.merge("inputs" => target.status == "ok" ? target.inputs : nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The digest of the bytes the analysis read; a file that did not parse is read once more.
|
|
90
|
+
def file_digest(target, read)
|
|
91
|
+
return nil unless read
|
|
92
|
+
return target.reader.digest if target.reader
|
|
93
|
+
|
|
94
|
+
Lens.digest(File.binread(target.absolute))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def analysed_fields(target)
|
|
98
|
+
reader = target&.reader
|
|
99
|
+
{ "complete" => reader && reader.omissions.empty?, "omissions" => reader&.omissions,
|
|
100
|
+
"sites" => reader&.sites&.map { |site| site.data.slice(*SITE_FIELDS) } }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def diagnostic_row(diagnostic)
|
|
104
|
+
{ "rule" => diagnostic.qualified_rule, "severity" => diagnostic.severity.to_s,
|
|
105
|
+
"message" => diagnostic.message, "line" => diagnostic.line, "column" => diagnostic.column }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def relative(absolute)
|
|
109
|
+
Pathname(absolute).relative_path_from(Pathname(@root)).to_s
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|