rigortype 0.1.18 → 0.1.19
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +266 -63
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
- data/lib/rigor/analysis/runner.rb +58 -21
- data/lib/rigor/analysis/worker_session.rb +21 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +3 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +57 -1
- data/lib/rigor/environment/rbs_loader.rb +25 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1052 -43
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +72 -1
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +270 -37
- data/lib/rigor/inference/scope_indexer.rb +696 -25
- data/lib/rigor/inference/statement_evaluator.rb +963 -16
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/base.rb +235 -79
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +12 -11
- data/lib/rigor/scope/discovery_index.rb +2 -0
- data/lib/rigor/scope.rb +132 -6
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +25 -0
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +10 -1
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +7 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -43,7 +43,7 @@ module Rigor
|
|
|
43
43
|
|
|
44
44
|
attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
|
|
45
45
|
:owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
|
|
46
|
-
:heredoc_templates, :nested_class_templates, :trait_registries,
|
|
46
|
+
:heredoc_templates, :nested_class_templates, :trait_registries,
|
|
47
47
|
:hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
|
|
48
48
|
:source_rbs_synthesizer, :additional_initializers
|
|
49
49
|
|
|
@@ -52,7 +52,7 @@ module Rigor
|
|
|
52
52
|
description: nil, config_schema: {},
|
|
53
53
|
produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
|
|
54
54
|
block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
|
|
55
|
-
trait_registries: [],
|
|
55
|
+
trait_registries: [],
|
|
56
56
|
hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
|
|
57
57
|
source_rbs_synthesizer: nil, additional_initializers: []
|
|
58
58
|
)
|
|
@@ -67,7 +67,6 @@ module Rigor
|
|
|
67
67
|
validate_heredoc_templates!(heredoc_templates)
|
|
68
68
|
validate_nested_class_templates!(nested_class_templates)
|
|
69
69
|
validate_trait_registries!(trait_registries)
|
|
70
|
-
validate_external_files!(external_files)
|
|
71
70
|
validate_hkt_registrations!(hkt_registrations)
|
|
72
71
|
validate_hkt_definitions!(hkt_definitions)
|
|
73
72
|
validate_signature_paths!(signature_paths)
|
|
@@ -77,7 +76,7 @@ module Rigor
|
|
|
77
76
|
|
|
78
77
|
assign_fields(id, version, description, config_schema, produces, consumes, owns_receivers,
|
|
79
78
|
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
80
|
-
|
|
79
|
+
hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
81
80
|
source_rbs_synthesizer)
|
|
82
81
|
assign_nested_class_templates(nested_class_templates)
|
|
83
82
|
assign_additional_initializers(additional_initializers)
|
|
@@ -89,7 +88,7 @@ module Rigor
|
|
|
89
88
|
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
90
89
|
def assign_fields(id, version, description, config_schema, produces, consumes, owns_receivers,
|
|
91
90
|
open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
|
|
92
|
-
|
|
91
|
+
hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
|
|
93
92
|
source_rbs_synthesizer)
|
|
94
93
|
@id = id.dup.freeze
|
|
95
94
|
@version = version.dup.freeze
|
|
@@ -104,7 +103,6 @@ module Rigor
|
|
|
104
103
|
@block_as_methods = block_as_methods.dup.freeze
|
|
105
104
|
@heredoc_templates = heredoc_templates.dup.freeze
|
|
106
105
|
@trait_registries = trait_registries.dup.freeze
|
|
107
|
-
@external_files = external_files.dup.freeze
|
|
108
106
|
@hkt_registrations = hkt_registrations.dup.freeze
|
|
109
107
|
@hkt_definitions = hkt_definitions.dup.freeze
|
|
110
108
|
@signature_paths = signature_paths.map { |p| p.to_s.dup.freeze }.freeze
|
|
@@ -170,7 +168,6 @@ module Rigor
|
|
|
170
168
|
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
171
169
|
"nested_class_templates" => nested_class_templates.map(&:to_h),
|
|
172
170
|
"trait_registries" => trait_registries.map(&:to_h),
|
|
173
|
-
"external_files" => external_files.map(&:to_h),
|
|
174
171
|
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
175
172
|
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } },
|
|
176
173
|
"signature_paths" => signature_paths,
|
|
@@ -389,23 +386,6 @@ module Rigor
|
|
|
389
386
|
"Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
|
|
390
387
|
end
|
|
391
388
|
|
|
392
|
-
# ADR-16 slice 5a — `external_files:` declares the Tier D
|
|
393
|
-
# substrate entries (external-Ruby-file inclusion under a
|
|
394
|
-
# declared `self`). Slice 5a carries the declarations on
|
|
395
|
-
# the manifest; the engine integration that walks the
|
|
396
|
-
# matched files + narrows their entry scope is **queued for
|
|
397
|
-
# slice 5b**, gated on demonstrated demand from concrete
|
|
398
|
-
# plugin targets (Redmine webhook payloads, tDiary plugin
|
|
399
|
-
# loader, etc.). Plugin authors MAY declare entries today;
|
|
400
|
-
# the substrate does not yet act on them.
|
|
401
|
-
def validate_external_files!(entries)
|
|
402
|
-
return if entries.is_a?(Array) && entries.all?(Macro::ExternalFile)
|
|
403
|
-
|
|
404
|
-
raise ArgumentError,
|
|
405
|
-
"plugin manifest external_files must be an Array of " \
|
|
406
|
-
"Rigor::Plugin::Macro::ExternalFile instances, got #{entries.inspect}"
|
|
407
|
-
end
|
|
408
|
-
|
|
409
389
|
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
410
390
|
# Lightweight HKT URI registrations this plugin ships
|
|
411
391
|
# (analogous to `%a{rigor:v1:hkt_register: ...}` directives
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "node_context"
|
|
4
4
|
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "../analysis/check_rules/rule_walk"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Plugin
|
|
@@ -64,30 +65,74 @@ module Rigor
|
|
|
64
65
|
# Walk `root` once, dispatching every node to each matching
|
|
65
66
|
# `(plugin, rule)`. Returns an Array of {Result} in plugin
|
|
66
67
|
# (registry) order. `root` nil yields one empty Result per plugin.
|
|
67
|
-
|
|
68
|
+
#
|
|
69
|
+
# ADR-53 B4 — when `collector_driver` is given (an
|
|
70
|
+
# {Analysis::CheckRules::RuleWalk::CollectorDriver}), the SAME
|
|
71
|
+
# single traversal also drives the built-in {CheckRules} node
|
|
72
|
+
# collectors: each visited node is dispatched both to the plugin
|
|
73
|
+
# rules (this walk's original job) and to the built-in collectors
|
|
74
|
+
# (the `CollectorDriver`), so a file is walked once for both
|
|
75
|
+
# instead of once each. The two dispatch models coexist: plugin
|
|
76
|
+
# rules keep `is_a?` matching via the per-class memo and receive a
|
|
77
|
+
# lazily-built {NodeContext} (ancestors); built-in collectors keep
|
|
78
|
+
# exact-node-class dispatch and receive the immutable
|
|
79
|
+
# {RuleWalk::Context} threaded through the descent. Order is
|
|
80
|
+
# preserved because each side accumulates into its own bucket
|
|
81
|
+
# (per-plugin {Result}s / per-collector `results`) and the two are
|
|
82
|
+
# assembled separately by their respective diagnostic builders.
|
|
83
|
+
# A raising plugin rule isolates only that plugin (per-{State}
|
|
84
|
+
# rescue) and never aborts built-in collection, nor vice versa
|
|
85
|
+
# (the collectors' `visit` is the verbatim legacy gather logic,
|
|
86
|
+
# which does not raise on the corpora).
|
|
87
|
+
def diagnostics_for_file(path:, scope:, root:, collector_driver: nil)
|
|
68
88
|
return @entries.map { |plugin, _| Result.new(plugin, [], nil) } if root.nil?
|
|
69
89
|
|
|
70
90
|
states = @entries.map { |plugin, rules| State.new(plugin, rules, scope, root) }
|
|
71
|
-
walk(path, scope, root, states)
|
|
91
|
+
walk(path, scope, root, states, collector_driver)
|
|
72
92
|
states.map(&:result)
|
|
73
93
|
end
|
|
74
94
|
|
|
75
95
|
private
|
|
76
96
|
|
|
77
|
-
def walk(path, scope, root, states)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
def walk(path, scope, root, states, collector_driver)
|
|
98
|
+
context = collector_driver ? Analysis::CheckRules::RuleWalk::Context.root : nil
|
|
99
|
+
walk_node(root, [], context, path, scope, states, collector_driver)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The single converged DFS pre-order traversal. Threads both the
|
|
103
|
+
# live `ancestors` stack (for plugin {NodeContext}) and the
|
|
104
|
+
# immutable built-in {RuleWalk::Context} (for the collectors),
|
|
105
|
+
# derived together as the walk descends — the cheap-ancestors
|
|
106
|
+
# option from the ADR-53 B4 design note. Identical pre-order over
|
|
107
|
+
# `compact_child_nodes` to both the legacy
|
|
108
|
+
# `Source::NodeWalker.each_with_ancestors` and `RuleWalk.walk`, so
|
|
109
|
+
# every node is visited in the same order each side saw before.
|
|
110
|
+
def walk_node(node, ancestors, context, path, scope, states, collector_driver)
|
|
111
|
+
return unless node.is_a?(Prism::Node)
|
|
112
|
+
|
|
113
|
+
dispatch_plugins(node, ancestors, path, scope, states)
|
|
114
|
+
collector_driver&.visit(node, context)
|
|
115
|
+
|
|
116
|
+
child_context = collector_driver&.descend(node, context)
|
|
117
|
+
ancestors.push(node)
|
|
118
|
+
node.compact_child_nodes.each do |child|
|
|
119
|
+
walk_node(child, ancestors, child_context, path, scope, states, collector_driver)
|
|
120
|
+
end
|
|
121
|
+
ancestors.pop
|
|
122
|
+
end
|
|
82
123
|
|
|
83
|
-
|
|
84
|
-
|
|
124
|
+
def dispatch_plugins(node, ancestors, path, scope, states)
|
|
125
|
+
node_context = nil
|
|
126
|
+
states.each do |state|
|
|
127
|
+
next if state.failed?
|
|
85
128
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
matched = state.rules_for(node)
|
|
130
|
+
next if matched.empty?
|
|
131
|
+
|
|
132
|
+
# One frozen NodeContext per node, built lazily and shared
|
|
133
|
+
# across every plugin that matches this node.
|
|
134
|
+
node_context ||= NodeContext.new(ancestors)
|
|
135
|
+
state.run_rules(matched, node, scope, path, node_context)
|
|
91
136
|
end
|
|
92
137
|
end
|
|
93
138
|
|
|
@@ -7,7 +7,7 @@ module Rigor
|
|
|
7
7
|
# ADR-52 WD1 — the compiled contribution table. Categorises a loaded
|
|
8
8
|
# plugin set by which per-call contribution paths each plugin
|
|
9
9
|
# actually implements, AND compiles the declarative gates (method
|
|
10
|
-
# names, `block_as_methods`
|
|
10
|
+
# names, `block_as_methods` method names, `owns_receivers`) into frozen
|
|
11
11
|
# lookup structures, so the engine's hot sites discover "no plugin
|
|
12
12
|
# cares about this call" in O(1) instead of O(plugins × rules) — a
|
|
13
13
|
# top hotspot on plugin-heavy projects (GitLab's 11 plugins, of
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
#
|
|
17
17
|
# Ordering contract: the gates only PRUNE consultations that could
|
|
18
18
|
# not fire (every pruned rule would have failed its own `methods:` /
|
|
19
|
-
# `
|
|
19
|
+
# `method_names:` check); the engine still iterates the plugin subsets in
|
|
20
20
|
# registry order and each plugin's rules in declaration order, so
|
|
21
21
|
# the surviving contributions arrive in exactly the order the
|
|
22
22
|
# ungated walk produced — diagnostics stay byte-identical. The
|
|
@@ -36,7 +36,7 @@ module Rigor
|
|
|
36
36
|
def initialize(plugins)
|
|
37
37
|
compile_memberships(plugins)
|
|
38
38
|
compile_gates
|
|
39
|
-
@
|
|
39
|
+
@block_entries_by_method_name = build_block_entries(plugins)
|
|
40
40
|
@owns_receivers = plugins.flat_map { |p| manifest_for(p)&.owns_receivers || [] }.uniq.freeze
|
|
41
41
|
# Per-run ancestry verdict memo, keyed by environment identity
|
|
42
42
|
# then class name. Mutable inside the frozen index — sound
|
|
@@ -88,11 +88,12 @@ module Rigor
|
|
|
88
88
|
gate.nil? || gate.include?(method_name)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
# The `Macro::BlockAsMethod` entries whose `
|
|
92
|
-
# in (plugin registration, manifest declaration)
|
|
93
|
-
# first-match order the previous plugins ×
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
# The `Macro::BlockAsMethod` entries whose `method_names` include
|
|
92
|
+
# `method_name`, in (plugin registration, manifest declaration)
|
|
93
|
+
# order — the same first-match order the previous plugins ×
|
|
94
|
+
# entries walk visited.
|
|
95
|
+
def block_entries_for(method_name)
|
|
96
|
+
@block_entries_by_method_name.fetch(method_name, EMPTY_BLOCK_ENTRIES)
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
# True when `class_name` equals or inherits from any plugin's
|
|
@@ -188,15 +189,15 @@ module Rigor
|
|
|
188
189
|
gates.values.reduce(Set.new) { |acc, names| acc.merge(names) }.freeze
|
|
189
190
|
end
|
|
190
191
|
|
|
191
|
-
# `
|
|
192
|
-
# (plugin, declaration).
|
|
192
|
+
# `method-name Symbol → [BlockAsMethod entries]`, insertion-ordered
|
|
193
|
+
# by (plugin, declaration). Method names are Symbol-normalised by
|
|
193
194
|
# `Macro::BlockAsMethod#initialize`.
|
|
194
195
|
def build_block_entries(plugins)
|
|
195
196
|
table = {}
|
|
196
197
|
plugins.each do |plugin|
|
|
197
198
|
entries = manifest_for(plugin)&.block_as_methods || []
|
|
198
199
|
entries.each do |entry|
|
|
199
|
-
entry.
|
|
200
|
+
entry.method_names.each { |name| (table[name] ||= []) << entry }
|
|
200
201
|
end
|
|
201
202
|
end
|
|
202
203
|
table.each_value(&:freeze)
|
|
@@ -22,6 +22,7 @@ module Rigor
|
|
|
22
22
|
:in_source_constants,
|
|
23
23
|
:discovered_methods,
|
|
24
24
|
:discovered_def_nodes,
|
|
25
|
+
:discovered_singleton_def_nodes,
|
|
25
26
|
:discovered_def_sources,
|
|
26
27
|
:discovered_method_visibilities,
|
|
27
28
|
:discovered_superclasses,
|
|
@@ -46,6 +47,7 @@ module Rigor
|
|
|
46
47
|
in_source_constants: EMPTY_TABLE,
|
|
47
48
|
discovered_methods: EMPTY_TABLE,
|
|
48
49
|
discovered_def_nodes: EMPTY_TABLE,
|
|
50
|
+
discovered_singleton_def_nodes: EMPTY_TABLE,
|
|
49
51
|
discovered_def_sources: EMPTY_TABLE,
|
|
50
52
|
discovered_method_visibilities: EMPTY_TABLE,
|
|
51
53
|
discovered_superclasses: EMPTY_TABLE,
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Rigor
|
|
|
22
22
|
attr_reader :environment, :locals, :fact_store, :self_type,
|
|
23
23
|
:ivars, :cvars, :globals,
|
|
24
24
|
:indexed_narrowings, :method_chain_narrowings,
|
|
25
|
+
:declaration_sourced,
|
|
25
26
|
:source_path, :discovery
|
|
26
27
|
|
|
27
28
|
# ADR-53 Track A — the seed-time discovery tables live on the
|
|
@@ -43,6 +44,7 @@ module Rigor
|
|
|
43
44
|
def in_source_constants = @discovery.in_source_constants
|
|
44
45
|
def discovered_methods = @discovery.discovered_methods
|
|
45
46
|
def discovered_def_nodes = @discovery.discovered_def_nodes
|
|
47
|
+
def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes
|
|
46
48
|
def discovered_def_sources = @discovery.discovered_def_sources
|
|
47
49
|
def discovered_method_visibilities = @discovery.discovered_method_visibilities
|
|
48
50
|
def discovered_superclasses = @discovery.discovered_superclasses
|
|
@@ -86,8 +88,19 @@ module Rigor
|
|
|
86
88
|
EMPTY_VAR_BINDINGS = {}.freeze
|
|
87
89
|
EMPTY_INDEXED_NARROWINGS = {}.freeze
|
|
88
90
|
EMPTY_CHAIN_NARROWINGS = {}.freeze
|
|
91
|
+
# ADR-58 WD1 — the set of variable references whose binding's `nil`
|
|
92
|
+
# constituent is *declaration-sourced*: it arrives only via the
|
|
93
|
+
# class-ivar index seed (a ctor `@x = nil` written in another method),
|
|
94
|
+
# never through a method-local write, narrowing, or parameter. Members
|
|
95
|
+
# are frozen `[kind, name]` pairs (`[:ivar, :@x]`, `[:local, :r]`).
|
|
96
|
+
# `possible-nil-receiver` consults this set and declines to fire when the
|
|
97
|
+
# receiver's optionality is purely declaration-sourced — the working
|
|
98
|
+
# program's cross-method invariant is assumed per the robustness
|
|
99
|
+
# principle. Any flow-live touch (write / narrowing) drops the mark, so
|
|
100
|
+
# the diagnostic keeps firing exactly as before on flow-observed nil.
|
|
101
|
+
EMPTY_DECLARATION_SOURCED = Set.new.freeze
|
|
89
102
|
private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
|
|
90
|
-
:EMPTY_CHAIN_NARROWINGS
|
|
103
|
+
:EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED
|
|
91
104
|
|
|
92
105
|
class << self
|
|
93
106
|
def empty(environment: Environment.default, source_path: nil)
|
|
@@ -106,6 +119,7 @@ module Rigor
|
|
|
106
119
|
discovery: DiscoveryIndex::EMPTY,
|
|
107
120
|
indexed_narrowings: EMPTY_INDEXED_NARROWINGS,
|
|
108
121
|
method_chain_narrowings: EMPTY_CHAIN_NARROWINGS,
|
|
122
|
+
declaration_sourced: EMPTY_DECLARATION_SOURCED,
|
|
109
123
|
source_path: nil
|
|
110
124
|
)
|
|
111
125
|
@environment = environment
|
|
@@ -118,6 +132,7 @@ module Rigor
|
|
|
118
132
|
@discovery = discovery
|
|
119
133
|
@indexed_narrowings = indexed_narrowings
|
|
120
134
|
@method_chain_narrowings = method_chain_narrowings
|
|
135
|
+
@declaration_sourced = declaration_sourced
|
|
121
136
|
@source_path = source_path
|
|
122
137
|
freeze
|
|
123
138
|
end
|
|
@@ -141,9 +156,15 @@ module Rigor
|
|
|
141
156
|
# narrowing keyed on `(local, :x, :last)` no longer holds.
|
|
142
157
|
new_indexed_narrowings = drop_indexed_narrowings_for(:local, name)
|
|
143
158
|
new_chain_narrowings = drop_chain_narrowings_for(:local, name)
|
|
159
|
+
# ADR-58 WD1 — rebinding a local is a flow-live touch: any prior
|
|
160
|
+
# declaration-sourced mark on `name` no longer holds (the new value
|
|
161
|
+
# may carry a method-local nil). `with_declaration_sourced_local`
|
|
162
|
+
# re-establishes the mark afterward when the RHS is a pure copy of a
|
|
163
|
+
# declaration-sourced ivar read; the default is to drop it.
|
|
144
164
|
rebuild(locals: new_locals, fact_store: new_fact_store,
|
|
145
165
|
indexed_narrowings: new_indexed_narrowings,
|
|
146
|
-
method_chain_narrowings: new_chain_narrowings
|
|
166
|
+
method_chain_narrowings: new_chain_narrowings,
|
|
167
|
+
declaration_sourced: drop_declaration_sourced_for(:local, name))
|
|
147
168
|
end
|
|
148
169
|
|
|
149
170
|
def with_fact(fact)
|
|
@@ -201,9 +222,46 @@ module Rigor
|
|
|
201
222
|
def with_ivar(name, type)
|
|
202
223
|
new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
|
|
203
224
|
new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
|
|
225
|
+
# ADR-58 WD1 — a method-local ivar write or narrowing is flow-live:
|
|
226
|
+
# drop any declaration-sourced mark so subsequent reads of `@name`
|
|
227
|
+
# observe flow-live provenance and fire as before. The seed path uses
|
|
228
|
+
# `seed_declaration_sourced_ivar` to (re-)establish the mark.
|
|
204
229
|
rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
|
|
205
230
|
indexed_narrowings: new_indexed_narrowings,
|
|
206
|
-
method_chain_narrowings: new_chain_narrowings
|
|
231
|
+
method_chain_narrowings: new_chain_narrowings,
|
|
232
|
+
declaration_sourced: drop_declaration_sourced_for(:ivar, name))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only
|
|
236
|
+
# provenance is the class-ivar index. Unlike `with_ivar` this binds the
|
|
237
|
+
# type AND records the declaration-sourced mark in one transition.
|
|
238
|
+
def seed_declaration_sourced_ivar(name, type)
|
|
239
|
+
rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
|
|
240
|
+
declaration_sourced: add_declaration_sourced(:ivar, name))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# ADR-58 WD1 — a local assignment `r = @right` whose RHS is a pure read
|
|
244
|
+
# of a declaration-sourced ivar inherits the mark, so the survey's exact
|
|
245
|
+
# rotation/traversal shape (`r = @right; r.key`) does not fire. Binds the
|
|
246
|
+
# type and stamps the local's mark in one transition (the plain
|
|
247
|
+
# `with_local` would have dropped it).
|
|
248
|
+
def with_declaration_sourced_local(name, type)
|
|
249
|
+
written = with_local(name, type)
|
|
250
|
+
written.with_local_declaration_mark(name)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# ADR-58 WD1 — re-stamp the local mark on a scope produced by
|
|
254
|
+
# `with_local` (which always drops it). Public so the sibling
|
|
255
|
+
# `with_declaration_sourced_local` can call it across the new
|
|
256
|
+
# post-write receiver without reaching into a private method.
|
|
257
|
+
def with_local_declaration_mark(name)
|
|
258
|
+
rebuild(declaration_sourced: add_declaration_sourced(:local, name))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely
|
|
262
|
+
# declaration-sourced (no flow-live write/narrowing has touched it).
|
|
263
|
+
def declaration_sourced?(kind, name)
|
|
264
|
+
@declaration_sourced.include?([kind.to_sym, name.to_sym])
|
|
207
265
|
end
|
|
208
266
|
|
|
209
267
|
def with_cvar(name, type)
|
|
@@ -214,6 +272,23 @@ module Rigor
|
|
|
214
272
|
rebuild(globals: @globals.merge(name.to_sym => type).freeze)
|
|
215
273
|
end
|
|
216
274
|
|
|
275
|
+
# Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match
|
|
276
|
+
# and last-paren back-references). Narrowed on a successful-`=~` /
|
|
277
|
+
# `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`);
|
|
278
|
+
# any subsequent method call could run another match and rebind every
|
|
279
|
+
# one of them, so `eval_call` forgets the narrowed facts here. Always
|
|
280
|
+
# safe — only drops facts, so a subsequent read falls back to the
|
|
281
|
+
# default `String | nil`. Program-level `$GLOBAL = ...` seeds use other
|
|
282
|
+
# names and are untouched.
|
|
283
|
+
MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
|
|
284
|
+
private_constant :MATCH_DATA_GLOBALS
|
|
285
|
+
|
|
286
|
+
def forget_match_globals
|
|
287
|
+
return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) }
|
|
288
|
+
|
|
289
|
+
rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
|
|
290
|
+
end
|
|
291
|
+
|
|
217
292
|
# Slice 7 phase 2 — class-level ivar accumulator. Keyed by
|
|
218
293
|
# the qualified class name (e.g. `"Rigor::Scope"`); the
|
|
219
294
|
# value is a `Hash[Symbol, Type::t]` of every ivar that
|
|
@@ -263,8 +338,9 @@ module Rigor
|
|
|
263
338
|
# scope (no enclosing `class` / `module` body). True at the top
|
|
264
339
|
# of a file AND inside a top-level `def` body (since toplevel
|
|
265
340
|
# defs leave `self_type` nil per the existing scope-construction
|
|
266
|
-
# contract
|
|
267
|
-
#
|
|
341
|
+
# contract — the same nil-`self_type` signal ADR-24's self-call
|
|
342
|
+
# return adoption historically keyed on before ADR-57 opened the
|
|
343
|
+
# gate unconditionally). Used by
|
|
268
344
|
# `CheckRules#unresolved_toplevel_diagnostic` to gate the
|
|
269
345
|
# `call.unresolved-toplevel` rule so it fires only outside
|
|
270
346
|
# class / module bodies, where Rails-DSL metaprogramming
|
|
@@ -289,6 +365,22 @@ module Rigor
|
|
|
289
365
|
node
|
|
290
366
|
end
|
|
291
367
|
|
|
368
|
+
# Module-singleton call resolution (ADR-57 follow-up) — companion of
|
|
369
|
+
# {#user_def_for} for SINGLETON-side defs (`def self.x`, `def Foo.x`,
|
|
370
|
+
# `class << self` bodies, and `module_function` defs). Returns the
|
|
371
|
+
# `Prism::DefNode` for `class_name.method_name` invoked on the
|
|
372
|
+
# module/class constant itself, or nil. The `discovered_def_nodes`
|
|
373
|
+
# table is deliberately instance-side only (its ancestor walk binds
|
|
374
|
+
# `self` as `Nominal`), so singleton bodies live in a parallel table
|
|
375
|
+
# the `ScopeIndexer` populates alongside it. Records the same
|
|
376
|
+
# cross-file dependency edge as the instance path (ADR-46).
|
|
377
|
+
def singleton_def_for(class_name, method_name)
|
|
378
|
+
table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
|
|
379
|
+
node = table && table[method_name.to_sym]
|
|
380
|
+
record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active?
|
|
381
|
+
node
|
|
382
|
+
end
|
|
383
|
+
|
|
292
384
|
# ADR-46 slice 1 — note the cross-file dependency this resolution
|
|
293
385
|
# creates: the file defining `class_name#method_name` (the consumer's
|
|
294
386
|
# analysis reads its body via `infer_user_method_return`), or, when
|
|
@@ -564,7 +656,8 @@ module Rigor
|
|
|
564
656
|
@cvars == other.cvars &&
|
|
565
657
|
@globals == other.globals &&
|
|
566
658
|
@indexed_narrowings == other.indexed_narrowings &&
|
|
567
|
-
@method_chain_narrowings == other.method_chain_narrowings
|
|
659
|
+
@method_chain_narrowings == other.method_chain_narrowings &&
|
|
660
|
+
@declaration_sourced == other.declaration_sourced
|
|
568
661
|
end
|
|
569
662
|
alias eql? ==
|
|
570
663
|
|
|
@@ -580,6 +673,7 @@ module Rigor
|
|
|
580
673
|
discovery: @discovery,
|
|
581
674
|
indexed_narrowings: @indexed_narrowings,
|
|
582
675
|
method_chain_narrowings: @method_chain_narrowings,
|
|
676
|
+
declaration_sourced: @declaration_sourced,
|
|
583
677
|
source_path: @source_path
|
|
584
678
|
)
|
|
585
679
|
self.class.new(
|
|
@@ -589,6 +683,7 @@ module Rigor
|
|
|
589
683
|
discovery: discovery,
|
|
590
684
|
indexed_narrowings: indexed_narrowings,
|
|
591
685
|
method_chain_narrowings: method_chain_narrowings,
|
|
686
|
+
declaration_sourced: declaration_sourced,
|
|
592
687
|
source_path: source_path
|
|
593
688
|
)
|
|
594
689
|
end
|
|
@@ -621,10 +716,22 @@ module Rigor
|
|
|
621
716
|
discovery: @discovery,
|
|
622
717
|
indexed_narrowings: join_bindings(@indexed_narrowings, other.indexed_narrowings),
|
|
623
718
|
method_chain_narrowings: join_bindings(@method_chain_narrowings, other.method_chain_narrowings),
|
|
719
|
+
# ADR-58 WD1 — a ref is declaration-sourced after a join only when
|
|
720
|
+
# BOTH branches agree it is. If either path made the binding
|
|
721
|
+
# flow-live (a method-local nil write / failed-guard narrowing), the
|
|
722
|
+
# merge is flow-live and `possible-nil-receiver` fires as before.
|
|
723
|
+
declaration_sourced: join_declaration_sourced(other),
|
|
624
724
|
source_path: source_path
|
|
625
725
|
)
|
|
626
726
|
end
|
|
627
727
|
|
|
728
|
+
def join_declaration_sourced(other)
|
|
729
|
+
return @declaration_sourced if @declaration_sourced.equal?(other.declaration_sourced)
|
|
730
|
+
return EMPTY_DECLARATION_SOURCED if @declaration_sourced.empty? || other.declaration_sourced.empty?
|
|
731
|
+
|
|
732
|
+
(@declaration_sourced & other.declaration_sourced).freeze
|
|
733
|
+
end
|
|
734
|
+
|
|
628
735
|
def indexed_key(receiver_kind, receiver_name, key)
|
|
629
736
|
IndexedKey.new(
|
|
630
737
|
receiver_kind: receiver_kind.to_sym,
|
|
@@ -652,6 +759,25 @@ module Rigor
|
|
|
652
759
|
filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
|
|
653
760
|
end
|
|
654
761
|
|
|
762
|
+
# ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
|
|
763
|
+
def add_declaration_sourced(kind, name)
|
|
764
|
+
ref = [kind.to_sym, name.to_sym]
|
|
765
|
+
return @declaration_sourced if @declaration_sourced.include?(ref)
|
|
766
|
+
|
|
767
|
+
(@declaration_sourced.dup << ref).freeze
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def drop_declaration_sourced_for(kind, name)
|
|
771
|
+
return @declaration_sourced if @declaration_sourced.empty?
|
|
772
|
+
|
|
773
|
+
ref = [kind.to_sym, name.to_sym]
|
|
774
|
+
return @declaration_sourced unless @declaration_sourced.include?(ref)
|
|
775
|
+
|
|
776
|
+
dropped = @declaration_sourced.dup
|
|
777
|
+
dropped.delete(ref)
|
|
778
|
+
dropped.freeze
|
|
779
|
+
end
|
|
780
|
+
|
|
655
781
|
def drop_chain_narrowings_for(receiver_kind, receiver_name)
|
|
656
782
|
return @method_chain_narrowings if @method_chain_narrowings.empty?
|
|
657
783
|
|
|
@@ -118,7 +118,15 @@ module Rigor
|
|
|
118
118
|
@class_shells = Set.new
|
|
119
119
|
defs = collect_method_definitions(parse_result.value)
|
|
120
120
|
candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
|
|
121
|
+
# An analyzer bug typing one def's body must cost only that
|
|
122
|
+
# def's candidate, never the whole `rigor sig-gen` run. The
|
|
123
|
+
# `check` path recovers each *file* this way
|
|
124
|
+
# (worker_session.rb); sig-gen recovers per-def so the rest of
|
|
125
|
+
# the file's candidates still emit.
|
|
126
|
+
|
|
121
127
|
classify_def(path, def_node, class_name, kind, scope_index)
|
|
128
|
+
rescue StandardError
|
|
129
|
+
nil
|
|
122
130
|
end
|
|
123
131
|
obs_ivar_map = build_observed_ivar_map(parse_result.value)
|
|
124
132
|
candidates_from_defs + collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
|
|
@@ -304,26 +304,11 @@ module Rigor
|
|
|
304
304
|
m && [m[1], m[2]]
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
-
# Normalises a message receiver token to a class name.
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
# `singleton(Foo)` and bare `Foo` fold to `Foo`.
|
|
307
|
+
# Normalises a message receiver token to a class name. The fold
|
|
308
|
+
# logic is shared with the selector axis — see
|
|
309
|
+
# {Triage.normalize_receiver}.
|
|
311
310
|
def receiver_class(token)
|
|
312
|
-
|
|
313
|
-
return "Integer" if t.match?(/\A-?\d+\z/)
|
|
314
|
-
return "Float" if t.match?(/\A-?\d+\.\d+\z/)
|
|
315
|
-
return "String" if t.start_with?('"', "'")
|
|
316
|
-
return "Symbol" if t.start_with?(":")
|
|
317
|
-
|
|
318
|
-
singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
|
|
319
|
-
return singleton if singleton
|
|
320
|
-
return t if t.start_with?("Array[")
|
|
321
|
-
|
|
322
|
-
nominal = t[/\A([\w:]+)\[/, 1]
|
|
323
|
-
return nominal if nominal
|
|
324
|
-
return t if t.match?(/\A[\w:]+\z/)
|
|
325
|
-
|
|
326
|
-
nil
|
|
311
|
+
Triage.normalize_receiver(token)
|
|
327
312
|
end
|
|
328
313
|
|
|
329
314
|
def activesupport?(receiver, method)
|
data/lib/rigor/triage.rb
CHANGED
|
@@ -18,7 +18,15 @@ module Rigor
|
|
|
18
18
|
Summary = Data.define(:total, :error, :warning, :info)
|
|
19
19
|
RuleCount = Data.define(:rule, :count)
|
|
20
20
|
Hotspot = Data.define(:file, :count, :by_rule)
|
|
21
|
-
|
|
21
|
+
# A (receiver-class, method) dispatch target the diagnostics
|
|
22
|
+
# cluster on, built from the structured `Diagnostic#receiver_type`
|
|
23
|
+
# / `#method_name` fields — never from message-string parsing.
|
|
24
|
+
# `receiver` is nil for method-only diagnostics (a toplevel call,
|
|
25
|
+
# a `def`-side return / override finding that has no call
|
|
26
|
+
# receiver); `files` is the distinct-file count (a systemic vs.
|
|
27
|
+
# localised signal); `rules` is the per-rule breakdown.
|
|
28
|
+
Selector = Data.define(:receiver, :method_name, :count, :files, :rules)
|
|
29
|
+
Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints)
|
|
22
30
|
|
|
23
31
|
module_function
|
|
24
32
|
|
|
@@ -30,6 +38,7 @@ module Rigor
|
|
|
30
38
|
Report.new(
|
|
31
39
|
summary: build_summary(diagnostics),
|
|
32
40
|
distribution: build_distribution(diagnostics),
|
|
41
|
+
selectors: build_selectors(diagnostics),
|
|
33
42
|
hotspots: build_hotspots(diagnostics, top),
|
|
34
43
|
hints: hints ? Catalogue.recognise(diagnostics) : []
|
|
35
44
|
)
|
|
@@ -57,6 +66,61 @@ module Rigor
|
|
|
57
66
|
.sort_by { |row| [-row.count, row.rule] }
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
# The class/method aggregation axis (ADR-23 follow-up). Groups
|
|
70
|
+
# every diagnostic that carries a `method_name` by its
|
|
71
|
+
# `(receiver_type, method_name)` pair so a consumer can answer
|
|
72
|
+
# "which method / class concentrates the diagnostics?" with a
|
|
73
|
+
# `jq` query over the JSON instead of parsing message text.
|
|
74
|
+
# Method-only diagnostics (nil `receiver_type`) keep a null
|
|
75
|
+
# `receiver` and still group by method. The full list is
|
|
76
|
+
# returned uncapped — the JSON is the agent-facing surface; the
|
|
77
|
+
# text renderer caps its own rows.
|
|
78
|
+
def build_selectors(diagnostics)
|
|
79
|
+
diagnostics.select(&:method_name)
|
|
80
|
+
.group_by { |d| [normalize_receiver(d.receiver_type) || d.receiver_type, d.method_name.to_s] }
|
|
81
|
+
.map { |(receiver, method), group| selector_for(receiver, method, group) }
|
|
82
|
+
.sort_by { |s| [-s.count, s.receiver.to_s, s.method_name] }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Folds a receiver token — a `Diagnostic#receiver_type` display
|
|
86
|
+
# string or a message-parsed token — to the class the diagnostics
|
|
87
|
+
# should bucket under, so the selector axis does not fragment one
|
|
88
|
+
# method across every distinct literal receiver. String / integer
|
|
89
|
+
# / float / symbol literals collapse to their class; `singleton(C)`
|
|
90
|
+
# and a bare `C` fold to `C`; a generic `C[...]` keeps the
|
|
91
|
+
# `Array[String]` element form (the AR-relation heuristic keys on
|
|
92
|
+
# it). Returns nil for a token it cannot reduce to a class (a
|
|
93
|
+
# union display, an inferred shape) — the caller keeps the raw
|
|
94
|
+
# string then, never losing the row. Shared with {Catalogue}.
|
|
95
|
+
def normalize_receiver(token)
|
|
96
|
+
return nil if token.nil?
|
|
97
|
+
|
|
98
|
+
t = token.to_s.strip
|
|
99
|
+
return "Integer" if t.match?(/\A-?\d+\z/)
|
|
100
|
+
return "Float" if t.match?(/\A-?\d+\.\d+\z/)
|
|
101
|
+
return "String" if t.start_with?('"', "'")
|
|
102
|
+
return "Symbol" if t.start_with?(":")
|
|
103
|
+
|
|
104
|
+
singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1]
|
|
105
|
+
return singleton if singleton
|
|
106
|
+
return t if t.start_with?("Array[")
|
|
107
|
+
|
|
108
|
+
nominal = t[/\A([\w:]+)\[/, 1]
|
|
109
|
+
return nominal if nominal
|
|
110
|
+
return t if t.match?(/\A[\w:]+\z/)
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def selector_for(receiver, method, group)
|
|
116
|
+
rules = group.group_by { |d| rule_key(d) }
|
|
117
|
+
.transform_values(&:size)
|
|
118
|
+
.sort_by { |rule, count| [-count, rule] }
|
|
119
|
+
.to_h
|
|
120
|
+
Selector.new(receiver: receiver, method_name: method, count: group.size,
|
|
121
|
+
files: group.map(&:path).uniq.size, rules: rules)
|
|
122
|
+
end
|
|
123
|
+
|
|
60
124
|
def build_hotspots(diagnostics, top)
|
|
61
125
|
diagnostics.group_by(&:path)
|
|
62
126
|
.map { |path, group| hotspot_for(path, group) }
|
|
@@ -79,6 +143,10 @@ module Rigor
|
|
|
79
143
|
"warning" => report.summary.warning, "info" => report.summary.info
|
|
80
144
|
},
|
|
81
145
|
"distribution" => report.distribution.map { |r| { "rule" => r.rule, "count" => r.count } },
|
|
146
|
+
"selectors" => report.selectors.map do |s|
|
|
147
|
+
{ "receiver" => s.receiver, "method" => s.method_name, "count" => s.count,
|
|
148
|
+
"files" => s.files, "rules" => s.rules }
|
|
149
|
+
end,
|
|
82
150
|
"hotspots" => report.hotspots.map do |h|
|
|
83
151
|
{ "file" => h.file, "count" => h.count, "by_rule" => h.by_rule }
|
|
84
152
|
end,
|