rigortype 0.1.17 → 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 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- 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 +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -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 +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- 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/overload_selector.rb +33 -1
- 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 +187 -55
- 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 +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- 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 +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -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/analyzer.rb +1 -2
- 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/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- 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/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- 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/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- 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-ci-setup/SKILL.md +319 -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 +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -4,9 +4,9 @@ module Rigor
|
|
|
4
4
|
module Plugin
|
|
5
5
|
module Macro
|
|
6
6
|
# ADR-16 Tier A declaration: "the block passed to a
|
|
7
|
-
# class-level DSL call of one of `
|
|
8
|
-
# method on `receiver_constraint`'s subclass tree,
|
|
9
|
-
# `self` typed accordingly."
|
|
7
|
+
# class-level DSL call of one of `method_names` runs as an
|
|
8
|
+
# instance method on `receiver_constraint`'s subclass tree,
|
|
9
|
+
# with `self` typed accordingly."
|
|
10
10
|
#
|
|
11
11
|
# Authored on a plugin manifest:
|
|
12
12
|
#
|
|
@@ -16,7 +16,7 @@ module Rigor
|
|
|
16
16
|
# block_as_methods: [
|
|
17
17
|
# Rigor::Plugin::Macro::BlockAsMethod.new(
|
|
18
18
|
# receiver_constraint: "Sinatra::Base",
|
|
19
|
-
#
|
|
19
|
+
# method_names: %i[get post put delete head options patch link unlink]
|
|
20
20
|
# )
|
|
21
21
|
# ]
|
|
22
22
|
# )
|
|
@@ -41,9 +41,10 @@ module Rigor
|
|
|
41
41
|
# for the entry to fire. For Sinatra modular-style this is
|
|
42
42
|
# `"Sinatra::Base"`; the substrate's class-context match
|
|
43
43
|
# accepts every subclass.
|
|
44
|
-
# - `
|
|
44
|
+
# - `method_names` — Array of Symbol method names. A call shape
|
|
45
45
|
# `<receiver_subclass>.get('/path') { ... }` matches when
|
|
46
|
-
# `:get` is in this list.
|
|
46
|
+
# `:get` is in this list. (Named `verbs:` before ADR-60 WD2
|
|
47
|
+
# normalised the macro value-object vocabulary.)
|
|
47
48
|
# - `self_type` — Symbol selecting the kind of `self`-binding
|
|
48
49
|
# the substrate applies inside the block. Slice 1a accepts
|
|
49
50
|
# only `:receiver_instance` (the block runs as an instance
|
|
@@ -53,22 +54,22 @@ module Rigor
|
|
|
53
54
|
# ## Ractor-shareability
|
|
54
55
|
#
|
|
55
56
|
# All fields are frozen at construction (ADR-15 Phase 1).
|
|
56
|
-
# `
|
|
57
|
-
# not leak into the value. `Ractor.shareable?` returns
|
|
58
|
-
# after `#initialize`.
|
|
57
|
+
# `method_names` is dup-frozen so the caller's mutable array
|
|
58
|
+
# does not leak into the value. `Ractor.shareable?` returns
|
|
59
|
+
# true after `#initialize`.
|
|
59
60
|
class BlockAsMethod
|
|
60
61
|
SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
|
|
61
62
|
VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
|
|
62
63
|
|
|
63
|
-
attr_reader :receiver_constraint, :
|
|
64
|
+
attr_reader :receiver_constraint, :method_names, :self_type
|
|
64
65
|
|
|
65
|
-
def initialize(receiver_constraint:,
|
|
66
|
+
def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
|
|
66
67
|
validate_receiver_constraint!(receiver_constraint)
|
|
67
|
-
|
|
68
|
+
validate_method_names!(method_names)
|
|
68
69
|
validate_self_type!(self_type)
|
|
69
70
|
|
|
70
71
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
71
|
-
@
|
|
72
|
+
@method_names = method_names.map(&:to_sym).freeze
|
|
72
73
|
@self_type = self_type
|
|
73
74
|
freeze
|
|
74
75
|
end
|
|
@@ -76,7 +77,7 @@ module Rigor
|
|
|
76
77
|
def to_h
|
|
77
78
|
{
|
|
78
79
|
"receiver_constraint" => receiver_constraint,
|
|
79
|
-
"
|
|
80
|
+
"method_names" => method_names.map(&:to_s),
|
|
80
81
|
"self_type" => self_type.to_s
|
|
81
82
|
}
|
|
82
83
|
end
|
|
@@ -84,13 +85,13 @@ module Rigor
|
|
|
84
85
|
def ==(other)
|
|
85
86
|
other.is_a?(BlockAsMethod) &&
|
|
86
87
|
receiver_constraint == other.receiver_constraint &&
|
|
87
|
-
|
|
88
|
+
method_names == other.method_names &&
|
|
88
89
|
self_type == other.self_type
|
|
89
90
|
end
|
|
90
91
|
alias eql? ==
|
|
91
92
|
|
|
92
93
|
def hash
|
|
93
|
-
[receiver_constraint,
|
|
94
|
+
[receiver_constraint, method_names, self_type].hash
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
private
|
|
@@ -103,17 +104,17 @@ module Rigor
|
|
|
103
104
|
"got #{value.inspect}"
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
-
unless
|
|
107
|
+
def validate_method_names!(method_names)
|
|
108
|
+
unless method_names.is_a?(Array) && !method_names.empty?
|
|
108
109
|
raise ArgumentError,
|
|
109
|
-
"Plugin::Macro::BlockAsMethod#
|
|
110
|
+
"Plugin::Macro::BlockAsMethod#method_names must be a non-empty Array, got #{method_names.inspect}"
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
method_names.each do |v|
|
|
113
114
|
next if v.is_a?(Symbol) || (v.is_a?(String) && !v.empty?)
|
|
114
115
|
|
|
115
116
|
raise ArgumentError,
|
|
116
|
-
"Plugin::Macro::BlockAsMethod#
|
|
117
|
+
"Plugin::Macro::BlockAsMethod#method_names entries must be Symbol/non-empty String, " \
|
|
117
118
|
"got #{v.inspect}"
|
|
118
119
|
end
|
|
119
120
|
end
|
|
@@ -33,7 +33,7 @@ module Rigor
|
|
|
33
33
|
# receiver_constraint: "Mangrove::Enum", # `extend`-ed marker module
|
|
34
34
|
# block_method: :variants, # the enclosing DSL block
|
|
35
35
|
# variant_method: :variant, # each declaration call
|
|
36
|
-
#
|
|
36
|
+
# symbol_arg_position: 0, # constant arg → nested class
|
|
37
37
|
# inner_arg_position: 1, # type arg → `#inner` return
|
|
38
38
|
# inner_reader: :inner # the payload reader name
|
|
39
39
|
# )
|
|
@@ -48,8 +48,10 @@ module Rigor
|
|
|
48
48
|
# (`:variants`).
|
|
49
49
|
# - `variant_method` — Symbol naming each declaration call
|
|
50
50
|
# inside the block (`:variant`).
|
|
51
|
-
# - `
|
|
51
|
+
# - `symbol_arg_position` — Integer (default 0): the argument
|
|
52
52
|
# index whose literal **constant** names the nested subclass.
|
|
53
|
+
# (Named `name_arg_position:` before ADR-60 WD2 normalised
|
|
54
|
+
# the macro value-object vocabulary.)
|
|
53
55
|
# - `inner_arg_position` — Integer (default 1): the argument
|
|
54
56
|
# index whose type expression becomes the `#inner` reader's
|
|
55
57
|
# return type. Slice A resolves a constant type argument
|
|
@@ -69,21 +71,21 @@ module Rigor
|
|
|
69
71
|
# `Environment#class_ordering`.
|
|
70
72
|
class NestedClassTemplate
|
|
71
73
|
attr_reader :receiver_constraint, :block_method, :variant_method,
|
|
72
|
-
:
|
|
74
|
+
:symbol_arg_position, :inner_arg_position, :inner_reader
|
|
73
75
|
|
|
74
76
|
def initialize(receiver_constraint:, block_method: :variants, variant_method: :variant,
|
|
75
|
-
|
|
77
|
+
symbol_arg_position: 0, inner_arg_position: 1, inner_reader: :inner)
|
|
76
78
|
validate_constraint!(receiver_constraint)
|
|
77
79
|
validate_method!(block_method, "block_method")
|
|
78
80
|
validate_method!(variant_method, "variant_method")
|
|
79
|
-
validate_position!(
|
|
81
|
+
validate_position!(symbol_arg_position, "symbol_arg_position")
|
|
80
82
|
validate_position!(inner_arg_position, "inner_arg_position")
|
|
81
83
|
validate_method!(inner_reader, "inner_reader")
|
|
82
84
|
|
|
83
85
|
@receiver_constraint = receiver_constraint.dup.freeze
|
|
84
86
|
@block_method = block_method.to_sym
|
|
85
87
|
@variant_method = variant_method.to_sym
|
|
86
|
-
@
|
|
88
|
+
@symbol_arg_position = symbol_arg_position
|
|
87
89
|
@inner_arg_position = inner_arg_position
|
|
88
90
|
@inner_reader = inner_reader.to_sym
|
|
89
91
|
freeze
|
|
@@ -94,7 +96,7 @@ module Rigor
|
|
|
94
96
|
"receiver_constraint" => receiver_constraint,
|
|
95
97
|
"block_method" => block_method.to_s,
|
|
96
98
|
"variant_method" => variant_method.to_s,
|
|
97
|
-
"
|
|
99
|
+
"symbol_arg_position" => symbol_arg_position,
|
|
98
100
|
"inner_arg_position" => inner_arg_position,
|
|
99
101
|
"inner_reader" => inner_reader.to_s
|
|
100
102
|
}
|
data/lib/rigor/plugin/macro.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "macro/block_as_method"
|
|
4
|
-
require_relative "macro/external_file"
|
|
5
4
|
require_relative "macro/heredoc_template"
|
|
6
5
|
require_relative "macro/nested_class_template"
|
|
7
6
|
require_relative "macro/trait_registry"
|
|
@@ -11,8 +10,8 @@ module Rigor
|
|
|
11
10
|
# Substrate declarations for the macro / DSL expansion tiers
|
|
12
11
|
# introduced by ADR-16. Plugin authors declare entries under
|
|
13
12
|
# `Plugin::Manifest` slots (`block_as_methods:`,
|
|
14
|
-
# `trait_registries:`, `
|
|
15
|
-
# `
|
|
13
|
+
# `trait_registries:`, `heredoc_templates:`,
|
|
14
|
+
# `nested_class_templates:`) and the substrate consumes them
|
|
16
15
|
# to recognise the call shapes a library exposes to its users.
|
|
17
16
|
#
|
|
18
17
|
# Slice 1a (this file's first delivery) ships the Tier A value
|
|
@@ -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
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "node_context"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "../analysis/check_rules/rule_walk"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
# ADR-52 WD4 — one engine-owned AST walk per file for node rules.
|
|
10
|
+
#
|
|
11
|
+
# Before this, every plugin that declared a {Base.node_rule} walked
|
|
12
|
+
# the file's AST itself (`Base#node_rule_diagnostics` →
|
|
13
|
+
# `Source::NodeWalker.each_with_ancestors`), so a project with N
|
|
14
|
+
# node-rule plugins paid N walks per file. This folds them into a
|
|
15
|
+
# single walk that dispatches each visited node to every matching
|
|
16
|
+
# `(plugin, rule)` pair.
|
|
17
|
+
#
|
|
18
|
+
# Behaviour is preserved exactly so the diagnostics stay
|
|
19
|
+
# byte-identical (the WD6 gate):
|
|
20
|
+
#
|
|
21
|
+
# * Each plugin's `node_file_context` block runs once per file,
|
|
22
|
+
# before any of its rules fire, `instance_exec`'d on that plugin —
|
|
23
|
+
# same as the per-plugin walk.
|
|
24
|
+
# * One frozen {NodeContext} is built per node, lazily, only when at
|
|
25
|
+
# least one rule matches it. Because it wraps only the ancestors it
|
|
26
|
+
# is safe to share across plugins for the same node.
|
|
27
|
+
# * Each rule block is `instance_exec`'d on its own plugin instance
|
|
28
|
+
# with the same five arguments `(node, scope, path, file_context,
|
|
29
|
+
# context)`.
|
|
30
|
+
# * A plugin whose context block or any rule block raises has its
|
|
31
|
+
# whole node-rule contribution isolated — the walk records the
|
|
32
|
+
# error against that plugin and continues, matching the runner's
|
|
33
|
+
# per-plugin rescue around the old `#node_rule_diagnostics` call.
|
|
34
|
+
# * Diagnostics are bucketed per plugin and returned in the
|
|
35
|
+
# registry order the runner already iterates, so emission order is
|
|
36
|
+
# unchanged (plugin-major, not node-major) — order preservation is
|
|
37
|
+
# what keeps the gate byte-identical in this slice.
|
|
38
|
+
#
|
|
39
|
+
# The result is an ordered Array of {Result}, one per node-rule
|
|
40
|
+
# plugin (registry order). `Result#error` is non-nil iff that
|
|
41
|
+
# plugin's context or a rule block raised, in which case
|
|
42
|
+
# `#diagnostics` is empty; the runner turns the error into the same
|
|
43
|
+
# per-plugin `runtime-error` envelope it produced before.
|
|
44
|
+
class NodeRuleWalk
|
|
45
|
+
# One plugin's node-rule outcome for a single file. `error` is the
|
|
46
|
+
# exception raised by the plugin's context block or a rule block
|
|
47
|
+
# (nil on success); when set, `diagnostics` is empty.
|
|
48
|
+
Result = Struct.new(:plugin, :diagnostics, :error)
|
|
49
|
+
|
|
50
|
+
# Plugins that declare at least one `node_rule`, paired with their
|
|
51
|
+
# frozen rule list, in registry order. Built once per run and
|
|
52
|
+
# reused for every file.
|
|
53
|
+
def initialize(plugins)
|
|
54
|
+
@entries = plugins.filter_map do |plugin|
|
|
55
|
+
rules = plugin.class.node_rules
|
|
56
|
+
rules.empty? ? nil : [plugin, rules]
|
|
57
|
+
end.freeze
|
|
58
|
+
freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def empty?
|
|
62
|
+
@entries.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Walk `root` once, dispatching every node to each matching
|
|
66
|
+
# `(plugin, rule)`. Returns an Array of {Result} in plugin
|
|
67
|
+
# (registry) order. `root` nil yields one empty Result per plugin.
|
|
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)
|
|
88
|
+
return @entries.map { |plugin, _| Result.new(plugin, [], nil) } if root.nil?
|
|
89
|
+
|
|
90
|
+
states = @entries.map { |plugin, rules| State.new(plugin, rules, scope, root) }
|
|
91
|
+
walk(path, scope, root, states, collector_driver)
|
|
92
|
+
states.map(&:result)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
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
|
|
123
|
+
|
|
124
|
+
def dispatch_plugins(node, ancestors, path, scope, states)
|
|
125
|
+
node_context = nil
|
|
126
|
+
states.each do |state|
|
|
127
|
+
next if state.failed?
|
|
128
|
+
|
|
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)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Mutable per-(plugin, file) walk state. Kept private to the walk —
|
|
140
|
+
# holds the diagnostics bucket, the file context, the per-concrete-
|
|
141
|
+
# class match memo, and the isolation flag.
|
|
142
|
+
class State
|
|
143
|
+
def initialize(plugin, rules, scope, root)
|
|
144
|
+
@plugin = plugin
|
|
145
|
+
@rules = rules
|
|
146
|
+
@diagnostics = []
|
|
147
|
+
@match_cache = {}.compare_by_identity
|
|
148
|
+
@error = nil
|
|
149
|
+
build_file_context(scope, root)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def failed?
|
|
153
|
+
!@error.nil?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Rules whose `node_type` this concrete node satisfies, memoised
|
|
157
|
+
# by the node's class so the `is_a?` scan runs once per class.
|
|
158
|
+
# Preserves `is_a?` semantics when a rule's `node_type` is a
|
|
159
|
+
# superclass of the concrete node.
|
|
160
|
+
def rules_for(node)
|
|
161
|
+
@match_cache[node.class] ||=
|
|
162
|
+
@rules.select { |rule| node.is_a?(rule[:node_type]) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def run_rules(matched, node, scope, path, context)
|
|
166
|
+
matched.each do |rule|
|
|
167
|
+
result = @plugin.instance_exec(node, scope, path, @file_context, context, &rule[:block])
|
|
168
|
+
@diagnostics.concat(Array(result))
|
|
169
|
+
end
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
@error = e
|
|
172
|
+
@diagnostics = []
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def result
|
|
176
|
+
NodeRuleWalk::Result.new(@plugin, @diagnostics, @error)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def build_file_context(scope, root)
|
|
182
|
+
block = @plugin.class.node_file_context_block
|
|
183
|
+
@file_context = block ? @plugin.instance_exec(root, scope, &block) : nil
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
@error = e
|
|
186
|
+
@file_context = nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
private_constant :State
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|