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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -222
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
  9. data/lib/rigor/analysis/check_rules.rb +275 -44
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
  12. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  13. data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
  14. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  15. data/lib/rigor/analysis/runner.rb +207 -1200
  16. data/lib/rigor/analysis/worker_session.rb +60 -11
  17. data/lib/rigor/bleeding_edge.rb +123 -0
  18. data/lib/rigor/cache/descriptor.rb +86 -8
  19. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  20. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  21. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  22. data/lib/rigor/cache/store.rb +46 -13
  23. data/lib/rigor/cli/annotate_command.rb +100 -15
  24. data/lib/rigor/cli/check_command.rb +708 -0
  25. data/lib/rigor/cli/ci_detector.rb +94 -0
  26. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  27. data/lib/rigor/cli/plugins_command.rb +2 -4
  28. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  29. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  30. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  31. data/lib/rigor/cli/trace_command.rb +143 -0
  32. data/lib/rigor/cli/trace_renderer.rb +310 -0
  33. data/lib/rigor/cli/triage_command.rb +6 -3
  34. data/lib/rigor/cli/triage_renderer.rb +15 -1
  35. data/lib/rigor/cli.rb +21 -612
  36. data/lib/rigor/configuration/severity_profile.rb +13 -1
  37. data/lib/rigor/configuration.rb +66 -7
  38. data/lib/rigor/environment/rbs_loader.rb +78 -68
  39. data/lib/rigor/environment.rb +1 -1
  40. data/lib/rigor/inference/acceptance.rb +10 -0
  41. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  42. data/lib/rigor/inference/budget_trace.rb +29 -2
  43. data/lib/rigor/inference/expression_typer.rb +1080 -105
  44. data/lib/rigor/inference/flow_tracer.rb +180 -0
  45. data/lib/rigor/inference/macro_block_self_type.rb +11 -12
  46. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  47. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  48. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  49. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  50. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  51. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  52. data/lib/rigor/inference/method_dispatcher.rb +187 -55
  53. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  54. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  55. data/lib/rigor/inference/mutation_widening.rb +142 -0
  56. data/lib/rigor/inference/narrowing.rb +330 -37
  57. data/lib/rigor/inference/scope_indexer.rb +770 -39
  58. data/lib/rigor/inference/statement_evaluator.rb +998 -68
  59. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  60. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  61. data/lib/rigor/plugin/base.rb +517 -120
  62. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  63. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  64. data/lib/rigor/plugin/macro.rb +2 -3
  65. data/lib/rigor/plugin/manifest.rb +4 -24
  66. data/lib/rigor/plugin/node_rule_walk.rb +192 -0
  67. data/lib/rigor/plugin/registry.rb +264 -35
  68. data/lib/rigor/plugin.rb +1 -0
  69. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  70. data/lib/rigor/scope/discovery_index.rb +60 -0
  71. data/lib/rigor/scope.rb +199 -204
  72. data/lib/rigor/sig_gen/generator.rb +8 -0
  73. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  74. data/lib/rigor/source/literals.rb +14 -0
  75. data/lib/rigor/triage/catalogue.rb +4 -19
  76. data/lib/rigor/triage.rb +69 -1
  77. data/lib/rigor/type/combinator.rb +34 -0
  78. data/lib/rigor/version.rb +1 -1
  79. data/lib/rigor.rb +0 -1
  80. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  81. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  82. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  83. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  84. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  85. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  86. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
  87. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  88. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
  89. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  90. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  91. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  92. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  93. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  94. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  96. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  97. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  98. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  99. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  100. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
  101. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  102. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  103. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  104. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  105. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  106. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  107. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
  108. data/sig/rigor/analysis/fact_store.rbs +3 -0
  109. data/sig/rigor/environment.rbs +0 -2
  110. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  111. data/sig/rigor/inference.rbs +5 -0
  112. data/sig/rigor/plugin/base.rbs +6 -4
  113. data/sig/rigor/plugin/manifest.rbs +1 -2
  114. data/sig/rigor/scope.rbs +50 -29
  115. data/sig/rigor/source.rbs +1 -0
  116. data/sig/rigor/type.rbs +1 -0
  117. data/sig/rigor.rbs +1 -1
  118. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  119. data/skills/rigor-ci-setup/SKILL.md +319 -0
  120. data/skills/rigor-plugin-author/SKILL.md +6 -4
  121. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  122. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  123. metadata +21 -3
  124. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
  125. 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 `verbs` runs as an instance
8
- # method on `receiver_constraint`'s subclass tree, with
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
- # verbs: %i[get post put delete head options patch link unlink]
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
- # - `verbs` — Array of Symbol method names. A call shape
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
- # `verbs` is dup-frozen so the caller's mutable array does
57
- # not leak into the value. `Ractor.shareable?` returns true
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, :verbs, :self_type
64
+ attr_reader :receiver_constraint, :method_names, :self_type
64
65
 
65
- def initialize(receiver_constraint:, verbs:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
66
+ def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
66
67
  validate_receiver_constraint!(receiver_constraint)
67
- validate_verbs!(verbs)
68
+ validate_method_names!(method_names)
68
69
  validate_self_type!(self_type)
69
70
 
70
71
  @receiver_constraint = receiver_constraint.dup.freeze
71
- @verbs = verbs.map(&:to_sym).freeze
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
- "verbs" => verbs.map(&:to_s),
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
- verbs == other.verbs &&
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, verbs, self_type].hash
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 validate_verbs!(verbs)
107
- unless verbs.is_a?(Array) && !verbs.empty?
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#verbs must be a non-empty Array, got #{verbs.inspect}"
110
+ "Plugin::Macro::BlockAsMethod#method_names must be a non-empty Array, got #{method_names.inspect}"
110
111
  end
111
112
 
112
- verbs.each do |v|
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#verbs entries must be Symbol/non-empty String, " \
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
- # name_arg_position: 0, # constant arg → nested class
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
- # - `name_arg_position` — Integer (default 0): the argument
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
- :name_arg_position, :inner_arg_position, :inner_reader
74
+ :symbol_arg_position, :inner_arg_position, :inner_reader
73
75
 
74
76
  def initialize(receiver_constraint:, block_method: :variants, variant_method: :variant,
75
- name_arg_position: 0, inner_arg_position: 1, inner_reader: :inner)
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!(name_arg_position, "name_arg_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
- @name_arg_position = name_arg_position
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
- "name_arg_position" => name_arg_position,
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
  }
@@ -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:`, `heredoc_macros:`,
15
- # `external_file_inclusions:`) and the substrate consumes them
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, :external_files,
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: [], external_files: [],
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
- external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
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
- external_files, hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
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