rigortype 0.0.9 → 0.1.0
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 +40 -2
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +183 -4
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
- data/lib/rigor/cache/rbs_constant_table.rb +2 -2
- data/lib/rigor/cache/rbs_descriptor.rb +2 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/store.rb +2 -0
- data/lib/rigor/cli.rb +9 -3
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +51 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/expression_typer.rb +84 -5
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +95 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +105 -130
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +7 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- metadata +18 -1
|
@@ -33,10 +33,10 @@ module Rigor
|
|
|
33
33
|
loader.each_constant_decl do |name, entry|
|
|
34
34
|
translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
|
|
35
35
|
table[name] = translated unless translated.is_a?(Type::Bot)
|
|
36
|
-
rescue
|
|
36
|
+
rescue ::RBS::BaseError
|
|
37
37
|
# Skip entries whose RBS type fails to translate; the cache
|
|
38
38
|
# stays robust to a broken signature rather than corrupting
|
|
39
|
-
# the whole table.
|
|
39
|
+
# the whole table. Analyzer-internal errors propagate.
|
|
40
40
|
end
|
|
41
41
|
table
|
|
42
42
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rbs_descriptor"
|
|
4
|
+
require_relative "rbs_environment_marshal_patch"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Cache
|
|
8
|
+
# Cache producer that materialises the full
|
|
9
|
+
# `Hash<String, RBS::Definition>` for instance-side class
|
|
10
|
+
# definitions in the RBS environment, in a single cache
|
|
11
|
+
# entry. Mirrors the {RbsConstantTable} layout.
|
|
12
|
+
#
|
|
13
|
+
# ADR-7 § "Slice 6-D" carry-over and dogfooding feedback:
|
|
14
|
+
# the earlier per-class cache layout (one entry per class,
|
|
15
|
+
# ~1300 files) made warm runs *slower* than `--no-cache`
|
|
16
|
+
# because each `instance_definition` call paid disk-open +
|
|
17
|
+
# `Marshal.load` overhead and the in-memory
|
|
18
|
+
# `RBS::DefinitionBuilder.build_instance` was actually fast
|
|
19
|
+
# given a cached `RBS::Environment`. The single-blob layout
|
|
20
|
+
# collapses that to one `Marshal.load` per process; warm runs
|
|
21
|
+
# now match `--no-cache` timing while preserving the
|
|
22
|
+
# cross-process invalidation story.
|
|
23
|
+
#
|
|
24
|
+
# Marshal-cleanness of `RBS::Definition` is enabled by the
|
|
25
|
+
# v0.0.9 C2 `RBS::Location` patch.
|
|
26
|
+
class RbsInstanceDefinitions
|
|
27
|
+
PRODUCER_ID = "rbs.instance_definitions"
|
|
28
|
+
|
|
29
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
30
|
+
# @param store [Rigor::Cache::Store]
|
|
31
|
+
# @return [Hash{String => RBS::Definition}]
|
|
32
|
+
def self.fetch(loader:, store:)
|
|
33
|
+
descriptor = RbsDescriptor.build(loader)
|
|
34
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
35
|
+
compute(loader)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.compute(loader)
|
|
40
|
+
table = {}
|
|
41
|
+
loader.each_known_class_name do |name|
|
|
42
|
+
definition = loader.uncached_instance_definition(name)
|
|
43
|
+
table[name] = definition if definition
|
|
44
|
+
end
|
|
45
|
+
table
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :compute
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Singleton-side equivalent of {RbsInstanceDefinitions}.
|
|
52
|
+
# Caches the full `Hash<String, RBS::Definition>` for the
|
|
53
|
+
# singleton class of every RBS-known class.
|
|
54
|
+
class RbsSingletonDefinitions
|
|
55
|
+
PRODUCER_ID = "rbs.singleton_definitions"
|
|
56
|
+
|
|
57
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
58
|
+
# @param store [Rigor::Cache::Store]
|
|
59
|
+
# @return [Hash{String => RBS::Definition}]
|
|
60
|
+
def self.fetch(loader:, store:)
|
|
61
|
+
descriptor = RbsDescriptor.build(loader)
|
|
62
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
63
|
+
compute(loader)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.compute(loader)
|
|
68
|
+
table = {}
|
|
69
|
+
loader.each_known_class_name do |name|
|
|
70
|
+
definition = loader.uncached_singleton_definition(name)
|
|
71
|
+
table[name] = definition if definition
|
|
72
|
+
end
|
|
73
|
+
table
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method :compute
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/rigor/cache/store.rb
CHANGED
data/lib/rigor/cli.rb
CHANGED
|
@@ -211,9 +211,15 @@ module Rigor
|
|
|
211
211
|
# (no plugins are loaded today).
|
|
212
212
|
# - disable: list of `rigor check` rule identifiers to
|
|
213
213
|
# silence project-wide. The shipped rules are
|
|
214
|
-
# undefined-method, wrong-arity,
|
|
215
|
-
# argument-type-mismatch,
|
|
216
|
-
#
|
|
214
|
+
# call.undefined-method, call.wrong-arity,
|
|
215
|
+
# call.argument-type-mismatch,
|
|
216
|
+
# call.possible-nil-receiver, dump.type,
|
|
217
|
+
# assert.type-mismatch, flow.always-raises.
|
|
218
|
+
# A bare family token (`call`, `flow`,
|
|
219
|
+
# `assert`, `dump`, `def`) wildcards every
|
|
220
|
+
# rule under that prefix. Legacy unprefixed
|
|
221
|
+
# names (`undefined-method`, …) still
|
|
222
|
+
# resolve. In-source
|
|
217
223
|
# `# rigor:disable <rule>` comments at the end
|
|
218
224
|
# of an offending line silence per-line; use
|
|
219
225
|
# `# rigor:disable all` to suppress every rule.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Configuration
|
|
5
|
+
# ADR-8 § "Severity profile" — three named profiles tune the
|
|
6
|
+
# severity of every built-in `Analysis::CheckRules` rule for
|
|
7
|
+
# the run. Profiles are applied as a **final filter** on
|
|
8
|
+
# `Diagnostic#severity`: rules emit with their authored
|
|
9
|
+
# severity, then `Analysis::Runner` re-stamps the severity
|
|
10
|
+
# from the active profile before adding the diagnostic to
|
|
11
|
+
# the result.
|
|
12
|
+
#
|
|
13
|
+
# Three profiles:
|
|
14
|
+
#
|
|
15
|
+
# - `lenient`: Only proven (`:no`) diagnostics are errors;
|
|
16
|
+
# uncertain (`:maybe`) drop to `:warning`. Useful for
|
|
17
|
+
# incremental adoption on legacy code.
|
|
18
|
+
# - `balanced` (**default**): Current Rigor stance — most
|
|
19
|
+
# rules `:error`; `dump.type` `:info`; uncertain rules
|
|
20
|
+
# `:warning`.
|
|
21
|
+
# - `strict`: Every rule is `:error`. CI-friendly.
|
|
22
|
+
#
|
|
23
|
+
# The profile resolution order:
|
|
24
|
+
#
|
|
25
|
+
# 1. Profile-specific entry for the canonical rule id.
|
|
26
|
+
# 2. The diagnostic's own authored severity (the rule's
|
|
27
|
+
# default).
|
|
28
|
+
# 3. `:error` (catch-all so an unrecognised rule still emits
|
|
29
|
+
# visibly — the public-API drift spec catches the
|
|
30
|
+
# bookkeeping gap separately).
|
|
31
|
+
module SeverityProfile
|
|
32
|
+
VALID_PROFILES = %i[lenient balanced strict].freeze
|
|
33
|
+
VALID_SEVERITIES = %i[error warning info off].freeze
|
|
34
|
+
|
|
35
|
+
DEFAULT_PROFILE = :balanced
|
|
36
|
+
|
|
37
|
+
# Per-profile severity tables. Missing keys fall back to
|
|
38
|
+
# the diagnostic's authored severity (typically `:error`).
|
|
39
|
+
PROFILES = {
|
|
40
|
+
lenient: {
|
|
41
|
+
"call.undefined-method" => :error,
|
|
42
|
+
"call.wrong-arity" => :error,
|
|
43
|
+
"call.argument-type-mismatch" => :warning,
|
|
44
|
+
"call.possible-nil-receiver" => :warning,
|
|
45
|
+
"flow.always-raises" => :warning,
|
|
46
|
+
"assert.type-mismatch" => :error,
|
|
47
|
+
"dump.type" => :info,
|
|
48
|
+
"def.return-type-mismatch" => :warning
|
|
49
|
+
}.freeze,
|
|
50
|
+
balanced: {
|
|
51
|
+
"call.undefined-method" => :error,
|
|
52
|
+
"call.wrong-arity" => :error,
|
|
53
|
+
"call.argument-type-mismatch" => :error,
|
|
54
|
+
"call.possible-nil-receiver" => :error,
|
|
55
|
+
"flow.always-raises" => :error,
|
|
56
|
+
"assert.type-mismatch" => :error,
|
|
57
|
+
"dump.type" => :info,
|
|
58
|
+
"def.return-type-mismatch" => :warning
|
|
59
|
+
}.freeze,
|
|
60
|
+
strict: {
|
|
61
|
+
"call.undefined-method" => :error,
|
|
62
|
+
"call.wrong-arity" => :error,
|
|
63
|
+
"call.argument-type-mismatch" => :error,
|
|
64
|
+
"call.possible-nil-receiver" => :error,
|
|
65
|
+
"flow.always-raises" => :error,
|
|
66
|
+
"assert.type-mismatch" => :error,
|
|
67
|
+
"dump.type" => :error,
|
|
68
|
+
"def.return-type-mismatch" => :error
|
|
69
|
+
}.freeze
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
module_function
|
|
73
|
+
|
|
74
|
+
# Resolves the configured severity for a diagnostic given
|
|
75
|
+
# the active profile and any per-rule overrides.
|
|
76
|
+
#
|
|
77
|
+
# @param rule [String, nil] canonical rule id (`call.undefined-method`).
|
|
78
|
+
# @param authored_severity [Symbol] severity the rule emitted
|
|
79
|
+
# the diagnostic with (`:error`, `:warning`, `:info`).
|
|
80
|
+
# @param profile [Symbol] one of {VALID_PROFILES}; falls back
|
|
81
|
+
# to {DEFAULT_PROFILE} for unknown values.
|
|
82
|
+
# @param overrides [Hash{String => Symbol}] per-rule severity
|
|
83
|
+
# overrides from `.rigor.yml`'s `severity_overrides:` map.
|
|
84
|
+
# Keys are canonical rule ids; values are
|
|
85
|
+
# {VALID_SEVERITIES} symbols. Family-wildcard keys
|
|
86
|
+
# (`call`) match every rule under that prefix.
|
|
87
|
+
# @return [Symbol] the resolved severity. Returns `:off` to
|
|
88
|
+
# mean "drop the diagnostic entirely".
|
|
89
|
+
def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {})
|
|
90
|
+
return authored_severity if rule.nil?
|
|
91
|
+
|
|
92
|
+
override = overrides[rule] || family_override(rule, overrides)
|
|
93
|
+
return override.to_sym if override
|
|
94
|
+
|
|
95
|
+
profile_table = PROFILES[profile] || PROFILES.fetch(DEFAULT_PROFILE)
|
|
96
|
+
profile_table.fetch(rule, authored_severity)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def family_override(rule, overrides)
|
|
100
|
+
family = rule.split(".").first
|
|
101
|
+
return nil if family.nil?
|
|
102
|
+
|
|
103
|
+
overrides[family]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private_class_method :family_override
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
|
+
require_relative "configuration/severity_profile"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
|
-
class Configuration
|
|
8
|
+
class Configuration # rubocop:disable Metrics/ClassLength
|
|
7
9
|
DEFAULT_PATH = ".rigor.yml"
|
|
8
10
|
DEFAULTS = {
|
|
9
11
|
"target_ruby" => "4.0",
|
|
@@ -15,11 +17,19 @@ module Rigor
|
|
|
15
17
|
"fold_platform_specific_paths" => false,
|
|
16
18
|
"cache" => {
|
|
17
19
|
"path" => ".rigor/cache"
|
|
18
|
-
}
|
|
20
|
+
},
|
|
21
|
+
"plugins_io" => {
|
|
22
|
+
"network" => "disabled",
|
|
23
|
+
"allowed_paths" => []
|
|
24
|
+
},
|
|
25
|
+
"severity_profile" => "balanced",
|
|
26
|
+
"severity_overrides" => {}
|
|
19
27
|
}.freeze
|
|
20
28
|
|
|
21
29
|
attr_reader :target_ruby, :paths, :plugins, :cache_path, :disabled_rules,
|
|
22
|
-
:libraries, :signature_paths, :fold_platform_specific_paths
|
|
30
|
+
:libraries, :signature_paths, :fold_platform_specific_paths,
|
|
31
|
+
:plugins_io_network, :plugins_io_allowed_paths,
|
|
32
|
+
:severity_profile, :severity_overrides
|
|
23
33
|
|
|
24
34
|
def self.load(path = DEFAULT_PATH)
|
|
25
35
|
data = if File.exist?(path)
|
|
@@ -31,12 +41,15 @@ module Rigor
|
|
|
31
41
|
new(DEFAULTS.merge(data))
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
def initialize(data = DEFAULTS) # rubocop:disable Metrics/AbcSize
|
|
44
|
+
def initialize(data = DEFAULTS) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
35
45
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
46
|
+
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
36
47
|
|
|
37
48
|
@target_ruby = data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")).to_s
|
|
38
49
|
@paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s)
|
|
39
|
-
@plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map
|
|
50
|
+
@plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
|
|
51
|
+
coerce_plugin_entry(entry)
|
|
52
|
+
end.freeze
|
|
40
53
|
@disabled_rules = Array(data.fetch("disable", DEFAULTS.fetch("disable"))).map(&:to_s).freeze
|
|
41
54
|
@libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
|
|
42
55
|
sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
|
|
@@ -45,6 +58,14 @@ module Rigor
|
|
|
45
58
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
46
59
|
) == true
|
|
47
60
|
@cache_path = cache.fetch("path").to_s
|
|
61
|
+
@plugins_io_network = coerce_network_policy(plugins_io.fetch("network"))
|
|
62
|
+
@plugins_io_allowed_paths = Array(plugins_io.fetch("allowed_paths")).map(&:to_s).freeze
|
|
63
|
+
@severity_profile = coerce_severity_profile(
|
|
64
|
+
data.fetch("severity_profile", DEFAULTS.fetch("severity_profile"))
|
|
65
|
+
)
|
|
66
|
+
@severity_overrides = coerce_severity_overrides(
|
|
67
|
+
data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
|
|
68
|
+
)
|
|
48
69
|
end
|
|
49
70
|
|
|
50
71
|
def to_h
|
|
@@ -58,8 +79,91 @@ module Rigor
|
|
|
58
79
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
59
80
|
"cache" => {
|
|
60
81
|
"path" => cache_path
|
|
61
|
-
}
|
|
82
|
+
},
|
|
83
|
+
"plugins_io" => {
|
|
84
|
+
"network" => plugins_io_network.to_s,
|
|
85
|
+
"allowed_paths" => plugins_io_allowed_paths
|
|
86
|
+
},
|
|
87
|
+
"severity_profile" => severity_profile.to_s,
|
|
88
|
+
"severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] }
|
|
62
89
|
}
|
|
63
90
|
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Accepts either `"rigor-foo"` (gem-name shorthand) or
|
|
95
|
+
# `{ "gem" => "rigor-foo", "id" => "foo", "config" => {...} }`
|
|
96
|
+
# (full form). Returns the canonical hash form so the loader
|
|
97
|
+
# works against a single shape.
|
|
98
|
+
def coerce_plugin_entry(entry)
|
|
99
|
+
case entry
|
|
100
|
+
when String
|
|
101
|
+
entry.dup.freeze
|
|
102
|
+
when Hash
|
|
103
|
+
entry.to_h { |k, v| [k.to_s, v] }.freeze
|
|
104
|
+
else
|
|
105
|
+
raise ArgumentError,
|
|
106
|
+
"plugin configuration entry must be a String or Hash, got #{entry.inspect}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Slice 2 only accepts `:disabled` for the network policy. The
|
|
111
|
+
# YAML scalar may arrive as a String (`"disabled"`) or already
|
|
112
|
+
# as the Symbol; coerce to the canonical Symbol shape so the
|
|
113
|
+
# downstream `TrustPolicy` constructor stays strict.
|
|
114
|
+
#
|
|
115
|
+
# The accepted set is duplicated from
|
|
116
|
+
# {Rigor::Plugin::TrustPolicy::VALID_NETWORK_POLICIES} so
|
|
117
|
+
# `Configuration` does not require the plugin namespace at
|
|
118
|
+
# load time (Configuration is loaded before Plugin in
|
|
119
|
+
# `lib/rigor.rb`); the two stay in lockstep via spec.
|
|
120
|
+
VALID_NETWORK_POLICIES = %i[disabled].freeze
|
|
121
|
+
private_constant :VALID_NETWORK_POLICIES
|
|
122
|
+
|
|
123
|
+
def coerce_network_policy(value)
|
|
124
|
+
sym = value.to_sym
|
|
125
|
+
unless VALID_NETWORK_POLICIES.include?(sym)
|
|
126
|
+
raise ArgumentError,
|
|
127
|
+
"plugins_io.network must be one of #{VALID_NETWORK_POLICIES.inspect}, got #{value.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sym
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ADR-8 § "Severity profile" — accepts the canonical Symbol
|
|
134
|
+
# form or its String spelling; rejects unknown profile names
|
|
135
|
+
# so typos fail loudly.
|
|
136
|
+
def coerce_severity_profile(value)
|
|
137
|
+
sym = value.to_sym
|
|
138
|
+
unless SeverityProfile::VALID_PROFILES.include?(sym)
|
|
139
|
+
raise ArgumentError,
|
|
140
|
+
"severity_profile must be one of " \
|
|
141
|
+
"#{SeverityProfile::VALID_PROFILES.inspect}, got #{value.inspect}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
sym
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ADR-8 § "Severity profile" — `severity_overrides:` is a
|
|
148
|
+
# `{ rule => severity }` map. Keys are canonical rule ids
|
|
149
|
+
# (`call.undefined-method`) or family wildcards (`call`).
|
|
150
|
+
# Values are {SeverityProfile::VALID_SEVERITIES} symbols
|
|
151
|
+
# (`:error` / `:warning` / `:info` / `:off`). Unknown
|
|
152
|
+
# severities raise; unknown rule ids are silently kept (the
|
|
153
|
+
# override is inert until the rule lands).
|
|
154
|
+
def coerce_severity_overrides(value)
|
|
155
|
+
raise ArgumentError, "severity_overrides must be a Hash, got #{value.inspect}" unless value.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
value.to_h do |k, v|
|
|
158
|
+
sym = v.to_sym
|
|
159
|
+
unless SeverityProfile::VALID_SEVERITIES.include?(sym)
|
|
160
|
+
raise ArgumentError,
|
|
161
|
+
"severity_overrides[#{k.inspect}] must be one of " \
|
|
162
|
+
"#{SeverityProfile::VALID_SEVERITIES.inspect}, got #{v.inspect}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
[k.to_s, sym]
|
|
166
|
+
end.freeze
|
|
167
|
+
end
|
|
64
168
|
end
|
|
65
169
|
end
|
|
@@ -125,19 +125,42 @@ module Rigor
|
|
|
125
125
|
|
|
126
126
|
env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
127
127
|
env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
128
|
-
rescue
|
|
129
|
-
# fail-soft: a broken environment yields no names.
|
|
128
|
+
rescue ::RBS::BaseError
|
|
129
|
+
# fail-soft: a broken RBS environment yields no names.
|
|
130
|
+
# Analyzer-internal errors (NameError, NoMethodError,
|
|
131
|
+
# LoadError) are NOT swallowed — those are bugs and
|
|
132
|
+
# must surface so they don't hide silently the way the
|
|
133
|
+
# v0.0.9 cache `Cache::Descriptor` regression did.
|
|
130
134
|
end
|
|
131
135
|
|
|
132
136
|
# @return [RBS::Definition, nil] the resolved instance definition
|
|
133
137
|
# for `class_name`, or nil when the class is unknown or its
|
|
134
138
|
# definition cannot be built (RBS may raise on broken hierarchies;
|
|
135
139
|
# we fail-soft and return nil so the caller can fall back).
|
|
140
|
+
#
|
|
141
|
+
# When `cache_store` is set, the loader fetches the per-class
|
|
142
|
+
# definition through {Cache::RbsInstanceDefinitions.fetch} so
|
|
143
|
+
# subsequent runs (and other loaders sharing the same Store)
|
|
144
|
+
# skip the `RBS::DefinitionBuilder.build_instance` step.
|
|
145
|
+
# In-memory `@instance_definition_cache` keeps the per-process
|
|
146
|
+
# short-circuit on top.
|
|
136
147
|
def instance_definition(class_name)
|
|
137
148
|
key = class_name.to_s
|
|
138
149
|
return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
|
|
139
150
|
|
|
140
|
-
@instance_definition_cache[key] =
|
|
151
|
+
@instance_definition_cache[key] = if cache_store
|
|
152
|
+
cached_instance_definition(class_name)
|
|
153
|
+
else
|
|
154
|
+
build_instance_definition(class_name)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Public uncached accessor used by the cache producer
|
|
159
|
+
# ({Rigor::Cache::RbsInstanceDefinitions}). Avoids the
|
|
160
|
+
# `private_method_called` round-trip a `loader.send(...)`
|
|
161
|
+
# callsite would require.
|
|
162
|
+
def uncached_instance_definition(class_name)
|
|
163
|
+
build_instance_definition(class_name)
|
|
141
164
|
end
|
|
142
165
|
|
|
143
166
|
# @return [RBS::Definition::Method, nil]
|
|
@@ -153,11 +176,25 @@ module Rigor
|
|
|
153
176
|
# definition are the *class methods* of `class_name`, including
|
|
154
177
|
# those inherited from `Class` and `Module` for class types.
|
|
155
178
|
# Returns nil for unknown names and on RBS build errors (fail-soft).
|
|
179
|
+
#
|
|
180
|
+
# When `cache_store` is set, the loader fetches the per-class
|
|
181
|
+
# singleton definition through
|
|
182
|
+
# {Cache::RbsSingletonDefinitions.fetch}; the same caching
|
|
183
|
+
# discipline as {#instance_definition}.
|
|
156
184
|
def singleton_definition(class_name)
|
|
157
185
|
key = class_name.to_s
|
|
158
186
|
return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
|
|
159
187
|
|
|
160
|
-
@singleton_definition_cache[key] =
|
|
188
|
+
@singleton_definition_cache[key] = if cache_store
|
|
189
|
+
cached_singleton_definition(class_name)
|
|
190
|
+
else
|
|
191
|
+
build_singleton_definition(class_name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Public uncached accessor used by the cache producer.
|
|
196
|
+
def uncached_singleton_definition(class_name)
|
|
197
|
+
build_singleton_definition(class_name)
|
|
161
198
|
end
|
|
162
199
|
|
|
163
200
|
# @return [RBS::Definition::Method, nil] the class method on
|
|
@@ -214,7 +251,7 @@ module Rigor
|
|
|
214
251
|
# should keep using {#constant_type} for point lookups.
|
|
215
252
|
def constant_names
|
|
216
253
|
env.constant_decls.keys.map(&:to_s)
|
|
217
|
-
rescue
|
|
254
|
+
rescue ::RBS::BaseError
|
|
218
255
|
[]
|
|
219
256
|
end
|
|
220
257
|
|
|
@@ -229,8 +266,8 @@ module Rigor
|
|
|
229
266
|
env.constant_decls.each do |rbs_name, entry|
|
|
230
267
|
yield rbs_name.to_s, entry
|
|
231
268
|
end
|
|
232
|
-
rescue
|
|
233
|
-
# fail-soft: a broken environment yields no entries.
|
|
269
|
+
rescue ::RBS::BaseError
|
|
270
|
+
# fail-soft: a broken RBS environment yields no entries.
|
|
234
271
|
end
|
|
235
272
|
|
|
236
273
|
# Slice A constant-value lookup. Returns the translated
|
|
@@ -258,7 +295,7 @@ module Rigor
|
|
|
258
295
|
else
|
|
259
296
|
translate_constant_decl(rbs_name)
|
|
260
297
|
end
|
|
261
|
-
rescue
|
|
298
|
+
rescue ::RBS::BaseError
|
|
262
299
|
nil
|
|
263
300
|
end
|
|
264
301
|
|
|
@@ -290,7 +327,7 @@ module Rigor
|
|
|
290
327
|
return false unless rbs_name
|
|
291
328
|
|
|
292
329
|
known_class_names_set.include?(rbs_name.to_s)
|
|
293
|
-
rescue
|
|
330
|
+
rescue ::RBS::BaseError
|
|
294
331
|
false
|
|
295
332
|
end
|
|
296
333
|
|
|
@@ -311,6 +348,45 @@ module Rigor
|
|
|
311
348
|
Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
|
|
312
349
|
end
|
|
313
350
|
|
|
351
|
+
# Per-process Hash<String, RBS::Definition> for the instance
|
|
352
|
+
# side. Loaded once on first miss through the
|
|
353
|
+
# {Cache::RbsInstanceDefinitions} producer (single Marshal
|
|
354
|
+
# blob); subsequent calls are pure Hash lookups. Cold runs
|
|
355
|
+
# build every known class once and persist; warm runs (and
|
|
356
|
+
# other loaders sharing the same Store) skip the
|
|
357
|
+
# `RBS::DefinitionBuilder.build_instance` work entirely.
|
|
358
|
+
def cached_instance_definition(class_name)
|
|
359
|
+
instance_definitions_table[normalise_class_key(class_name)]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def instance_definitions_table
|
|
363
|
+
@state[:instance_definitions_table] ||= begin
|
|
364
|
+
require_relative "../cache/rbs_instance_definitions"
|
|
365
|
+
Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def cached_singleton_definition(class_name)
|
|
370
|
+
singleton_definitions_table[normalise_class_key(class_name)]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def singleton_definitions_table
|
|
374
|
+
@state[:singleton_definitions_table] ||= begin
|
|
375
|
+
require_relative "../cache/rbs_instance_definitions"
|
|
376
|
+
Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# The cache producers persist class names in
|
|
381
|
+
# `RBS::TypeName#to_s` form (top-level prefixed
|
|
382
|
+
# `"::Hash"`); plain-name lookups (`"Hash"`) normalise
|
|
383
|
+
# before the Hash query so callers stay agnostic to the
|
|
384
|
+
# prefix.
|
|
385
|
+
def normalise_class_key(class_name)
|
|
386
|
+
s = class_name.to_s
|
|
387
|
+
s.start_with?("::") ? s : "::#{s}"
|
|
388
|
+
end
|
|
389
|
+
|
|
314
390
|
def builder
|
|
315
391
|
@state[:builder] ||= RBS::DefinitionBuilder.new(env: env)
|
|
316
392
|
end
|
|
@@ -325,7 +401,7 @@ module Rigor
|
|
|
325
401
|
return nil unless env.class_decls.key?(rbs_name)
|
|
326
402
|
|
|
327
403
|
builder.build_instance(rbs_name)
|
|
328
|
-
rescue
|
|
404
|
+
rescue ::RBS::BaseError
|
|
329
405
|
nil
|
|
330
406
|
end
|
|
331
407
|
|
|
@@ -335,7 +411,7 @@ module Rigor
|
|
|
335
411
|
return nil unless env.class_decls.key?(rbs_name)
|
|
336
412
|
|
|
337
413
|
builder.build_singleton(rbs_name)
|
|
338
|
-
rescue
|
|
414
|
+
rescue ::RBS::BaseError
|
|
339
415
|
nil
|
|
340
416
|
end
|
|
341
417
|
|
|
@@ -345,7 +421,7 @@ module Rigor
|
|
|
345
421
|
|
|
346
422
|
s = "::#{s}" unless s.start_with?("::")
|
|
347
423
|
RBS::TypeName.parse(s)
|
|
348
|
-
rescue
|
|
424
|
+
rescue ::RBS::BaseError
|
|
349
425
|
nil
|
|
350
426
|
end
|
|
351
427
|
|
|
@@ -358,7 +434,7 @@ module Rigor
|
|
|
358
434
|
# them under one map post-resolution. Aliases live in their
|
|
359
435
|
# own table.
|
|
360
436
|
env.class_decls.key?(rbs_name) || env.class_alias_decls.key?(rbs_name)
|
|
361
|
-
rescue
|
|
437
|
+
rescue ::RBS::BaseError
|
|
362
438
|
false
|
|
363
439
|
end
|
|
364
440
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Records a contradiction between two or more flow contributions
|
|
6
|
+
# detected during {Merger.merge}. Carried on {MergeResult#conflicts}
|
|
7
|
+
# so the analyzer / formatter can surface a `:contribution_merge`
|
|
8
|
+
# diagnostic per ADR-2 § "Plugin Contribution Merging".
|
|
9
|
+
#
|
|
10
|
+
# ADR-2 § "Plugin Contribution Merging" rules out first-wins /
|
|
11
|
+
# last-wins behaviour: when contributions conflict, both sources
|
|
12
|
+
# are reported and the merger falls back to the nearest non-
|
|
13
|
+
# conflicting higher-tier (or default) value for the affected
|
|
14
|
+
# `(target, edge, kind)` slot. The conflict object is the
|
|
15
|
+
# carrier of that report.
|
|
16
|
+
#
|
|
17
|
+
# Slice-3 conflict reasons:
|
|
18
|
+
#
|
|
19
|
+
# - `:return_type_collapse` — two return-type contributions
|
|
20
|
+
# intersect to `bot`.
|
|
21
|
+
# - `:exceptional_disagreement` — two contributions assert
|
|
22
|
+
# incompatible non-`nil` exceptional effects.
|
|
23
|
+
# - `:lower_tier_contradiction` — a lower-tier contribution
|
|
24
|
+
# would weaken or contradict a higher-tier proof.
|
|
25
|
+
CONFLICT_VALID_REASONS = %i[
|
|
26
|
+
return_type_collapse
|
|
27
|
+
exceptional_disagreement
|
|
28
|
+
lower_tier_contradiction
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
Conflict = Data.define(:target, :edge, :kind, :reason, :provenances, :message) do
|
|
32
|
+
def initialize(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
|
|
33
|
+
unless CONFLICT_VALID_REASONS.include?(reason)
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"FlowContribution::Conflict reason must be one of " \
|
|
36
|
+
"#{CONFLICT_VALID_REASONS.inspect}, got #{reason.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(target: target, edge: edge, kind: kind, reason: reason,
|
|
40
|
+
provenances: provenances.dup.freeze, message: message.to_s.dup.freeze)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
"target" => target.to_s,
|
|
46
|
+
"edge" => edge.to_s,
|
|
47
|
+
"kind" => kind.to_s,
|
|
48
|
+
"reason" => reason.to_s,
|
|
49
|
+
"sources" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p.to_s },
|
|
50
|
+
"message" => message
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ADR-7 § "Slice 5-C" — converts the conflict into a
|
|
55
|
+
# `Rigor::Analysis::Diagnostic` for the run result.
|
|
56
|
+
# Carries `source_family: :contribution_merge` so the
|
|
57
|
+
# qualified-rule formatter (slice 5 formatter half,
|
|
58
|
+
# `ef730b2`) prefixes the rule id with
|
|
59
|
+
# `contribution_merge.` and the JSON output side-bands
|
|
60
|
+
# `source_family` + `rule` for plugin attribution.
|
|
61
|
+
#
|
|
62
|
+
# The `rule` identifier is the kebab-case form of the
|
|
63
|
+
# conflict reason (`return_type_collapse` →
|
|
64
|
+
# `return-type-collapse`, etc.) so the qualified rule
|
|
65
|
+
# reads `[contribution_merge.return-type-collapse]` in
|
|
66
|
+
# the standard text stream.
|
|
67
|
+
def to_diagnostic(path:, line:, column:, severity: :error)
|
|
68
|
+
require_relative "../analysis/diagnostic" unless defined?(Rigor::Analysis::Diagnostic)
|
|
69
|
+
Rigor::Analysis::Diagnostic.new(
|
|
70
|
+
path: path,
|
|
71
|
+
line: line,
|
|
72
|
+
column: column,
|
|
73
|
+
message: message,
|
|
74
|
+
severity: severity,
|
|
75
|
+
rule: reason.to_s.tr("_", "-"),
|
|
76
|
+
source_family: :contribution_merge
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|