rigortype 0.1.8 → 0.1.9

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -513
  3. data/lib/rigor/analysis/check_rules.rb +20 -0
  4. data/lib/rigor/cli/annotate_command.rb +224 -0
  5. data/lib/rigor/cli/baseline_command.rb +36 -16
  6. data/lib/rigor/cli/prism_colorizer.rb +111 -0
  7. data/lib/rigor/cli.rb +62 -4
  8. data/lib/rigor/environment.rb +9 -1
  9. data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
  10. data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
  11. data/lib/rigor/inference/expression_typer.rb +165 -6
  12. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
  13. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
  14. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
  15. data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
  16. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
  17. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
  18. data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
  19. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
  20. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
  21. data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
  22. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
  23. data/lib/rigor/inference/method_dispatcher.rb +148 -1
  24. data/lib/rigor/inference/method_parameter_binder.rb +67 -10
  25. data/lib/rigor/inference/narrowing.rb +29 -10
  26. data/lib/rigor/inference/statement_evaluator.rb +3 -1
  27. data/lib/rigor/plugin/base.rb +39 -0
  28. data/lib/rigor/plugin/loader.rb +22 -1
  29. data/lib/rigor/plugin/manifest.rb +73 -10
  30. data/lib/rigor/plugin/protocol_contract.rb +185 -0
  31. data/lib/rigor/plugin/registry.rb +66 -0
  32. data/lib/rigor/triage/catalogue.rb +2 -2
  33. data/lib/rigor/type/constant.rb +29 -2
  34. data/lib/rigor/version.rb +1 -1
  35. metadata +11 -1
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # ADR-28 declaration: "every instance/singleton method named
6
+ # `method_name`, defined in a source file matching `path_glob`,
7
+ # is implicitly required to satisfy the declared parameter +
8
+ # return-type protocol."
9
+ #
10
+ # Authored on a plugin manifest:
11
+ #
12
+ # manifest(
13
+ # id: "web",
14
+ # version: "0.1.0",
15
+ # protocol_contracts: [
16
+ # Rigor::Plugin::ProtocolContract.new(
17
+ # path_glob: "lib/controller/**/*.rb",
18
+ # method_name: :get,
19
+ # param_types: [{ index: 0, type_name: "Rack::Request" }],
20
+ # return_type_name: "Rack::Response"
21
+ # )
22
+ # ]
23
+ # )
24
+ #
25
+ # The contract drives two distinct engine behaviours (ADR-28
26
+ # § "provide-and-check"):
27
+ #
28
+ # - **provide** — when the inference engine binds the parameter
29
+ # list of a matching `def`, {Rigor::Inference::MethodParameterBinder}
30
+ # substitutes the declared `param_types` for the usual
31
+ # `Dynamic[Top]` fallback, so the method body is analysed as
32
+ # if the parameter carried its protocol type.
33
+ # - **check** — the contributing plugin's `#diagnostics_for_file`
34
+ # hook confirms the method exists and its inferred return type
35
+ # conforms to `return_type_name`.
36
+ #
37
+ # ## Fields
38
+ #
39
+ # - `path_glob` — `File.fnmatch` glob (String) selecting the
40
+ # source files the contract applies to, relative to the
41
+ # analysed project root (e.g. `"lib/controller/**/*.rb"`).
42
+ # - `method_name` — Symbol; the instance (or singleton) method
43
+ # the contract constrains.
44
+ # - `singleton` — Boolean; `true` constrains `def self.<name>`,
45
+ # `false` (default) constrains instance methods.
46
+ # - `param_types` — Array of `ParamType` (positional index →
47
+ # fully-qualified type name). The type names resolve against
48
+ # the analysed project's environment lazily, at consumption
49
+ # time, so the contract value object stays independent of
50
+ # environment construction order.
51
+ # - `return_type_name` — fully-qualified type name (String) the
52
+ # method's inferred return type must conform to.
53
+ # - `severity` — Symbol diagnostic severity for contract
54
+ # violations (`:error` default).
55
+ #
56
+ # ## Ractor-shareability
57
+ #
58
+ # Every field is frozen at construction (ADR-15 Phase 1); the
59
+ # nested `ParamType` is a frozen `Data`. `Ractor.shareable?`
60
+ # returns true after `#initialize`, so the contract survives
61
+ # `Plugin::Registry.materialize` into a worker Ractor.
62
+ class ProtocolContract
63
+ VALID_SEVERITIES = %i[error warning info].freeze
64
+
65
+ # One positional-parameter provision: the zero-based index of
66
+ # the parameter and the fully-qualified name of the type it
67
+ # carries under the protocol.
68
+ ParamType = Data.define(:index, :type_name)
69
+
70
+ attr_reader :path_glob, :method_name, :singleton, :param_types, :return_type_name, :severity
71
+
72
+ def initialize(path_glob:, method_name:, return_type_name: nil, param_types: [], singleton: false,
73
+ severity: :error)
74
+ validate_path_glob!(path_glob)
75
+ validate_method_name!(method_name)
76
+ validate_return_type_name!(return_type_name)
77
+ validate_severity!(severity)
78
+
79
+ @path_glob = path_glob.dup.freeze
80
+ @method_name = method_name.to_sym
81
+ @singleton = singleton ? true : false
82
+ @param_types = coerce_param_types(param_types)
83
+ @return_type_name = return_type_name.nil? ? nil : return_type_name.dup.freeze
84
+ @severity = severity.to_sym
85
+ freeze
86
+ end
87
+
88
+ # Returns a copy with `path_glob` replaced. Plugins use this to
89
+ # honour a per-project config override of the convention path
90
+ # without rebuilding the whole contract by hand.
91
+ def with_path_glob(glob)
92
+ ProtocolContract.new(
93
+ path_glob: glob,
94
+ method_name: method_name,
95
+ return_type_name: return_type_name,
96
+ param_types: param_types.map { |pt| { index: pt.index, type_name: pt.type_name } },
97
+ singleton: singleton,
98
+ severity: severity
99
+ )
100
+ end
101
+
102
+ def to_h
103
+ {
104
+ "path_glob" => path_glob,
105
+ "method_name" => method_name.to_s,
106
+ "singleton" => singleton,
107
+ "param_types" => param_types.map { |pt| { "index" => pt.index, "type_name" => pt.type_name } },
108
+ "return_type_name" => return_type_name,
109
+ "severity" => severity.to_s
110
+ }
111
+ end
112
+
113
+ def ==(other)
114
+ other.is_a?(ProtocolContract) && to_h == other.to_h
115
+ end
116
+ alias eql? ==
117
+
118
+ def hash
119
+ to_h.hash
120
+ end
121
+
122
+ private
123
+
124
+ def validate_path_glob!(value)
125
+ return if value.is_a?(String) && !value.empty?
126
+
127
+ raise ArgumentError,
128
+ "Plugin::ProtocolContract#path_glob must be a non-empty String, got #{value.inspect}"
129
+ end
130
+
131
+ def validate_method_name!(value)
132
+ return if value.is_a?(Symbol) || (value.is_a?(String) && !value.empty?)
133
+
134
+ raise ArgumentError,
135
+ "Plugin::ProtocolContract#method_name must be a Symbol/non-empty String, got #{value.inspect}"
136
+ end
137
+
138
+ def validate_return_type_name!(value)
139
+ return if value.nil?
140
+ return if value.is_a?(String) && !value.empty?
141
+
142
+ raise ArgumentError,
143
+ "Plugin::ProtocolContract#return_type_name must be a non-empty String or nil, got #{value.inspect}"
144
+ end
145
+
146
+ def validate_severity!(value)
147
+ return if VALID_SEVERITIES.include?(value.to_sym)
148
+
149
+ raise ArgumentError,
150
+ "Plugin::ProtocolContract#severity must be one of #{VALID_SEVERITIES.inspect}, got #{value.inspect}"
151
+ rescue NoMethodError
152
+ raise ArgumentError,
153
+ "Plugin::ProtocolContract#severity must be one of #{VALID_SEVERITIES.inspect}, got #{value.inspect}"
154
+ end
155
+
156
+ def coerce_param_types(param_types)
157
+ unless param_types.is_a?(Array)
158
+ raise ArgumentError,
159
+ "Plugin::ProtocolContract#param_types must be an Array, got #{param_types.inspect}"
160
+ end
161
+
162
+ param_types.map { |entry| coerce_param_type(entry) }.freeze
163
+ end
164
+
165
+ def coerce_param_type(entry)
166
+ return entry if entry.is_a?(ParamType)
167
+
168
+ unless entry.is_a?(Hash)
169
+ raise ArgumentError,
170
+ "Plugin::ProtocolContract param_types entry must be a Hash or ParamType, got #{entry.inspect}"
171
+ end
172
+
173
+ index = entry[:index] || entry["index"]
174
+ type_name = entry[:type_name] || entry["type_name"]
175
+ unless index.is_a?(Integer) && index >= 0 && type_name.is_a?(String) && !type_name.empty?
176
+ raise ArgumentError,
177
+ "Plugin::ProtocolContract param_types entry needs an Integer index >= 0 and a " \
178
+ "non-empty String type_name, got #{entry.inspect}"
179
+ end
180
+
181
+ ParamType.new(index: index, type_name: type_name.dup.freeze)
182
+ end
183
+ end
184
+ end
185
+ end
@@ -104,7 +104,73 @@ module Rigor
104
104
  Inference::HktRegistry.new(registrations: registrations, definitions: definitions)
105
105
  end
106
106
 
107
+ # ADR-25 — flat, ordered list of every loaded plugin's
108
+ # resolved RBS signature directories (absolute paths), in
109
+ # plugin registration order. `Environment.for_project`
110
+ # merges these into the signature-path set fed to
111
+ # `RbsLoader`, alongside the configuration's `signature_paths:`
112
+ # and the `bundler:` / `rbs_collection:` discovery output.
113
+ def signature_paths
114
+ plugins.flat_map(&:signature_paths)
115
+ end
116
+
117
+ # ADR-26 — the aggregate set of "open" receiver class names
118
+ # declared across loaded plugins (manifest `open_receivers:`).
119
+ # A class is open when a plugin vouches that it responds
120
+ # beyond its RBS-declared method surface. `open_receiver?`
121
+ # is the membership predicate `Analysis::CheckRules` consults
122
+ # to skip the `call.undefined-method` rule for such a class.
123
+ def open_receivers
124
+ plugins.flat_map { |plugin| plugin.manifest.open_receivers }
125
+ end
126
+
127
+ def open_receiver?(class_name)
128
+ return false if class_name.nil?
129
+
130
+ open_receivers.include?(class_name.to_s)
131
+ end
132
+
133
+ # ADR-28 — flat, ordered list of every loaded plugin's
134
+ # path-scoped method-protocol contracts, in plugin
135
+ # registration order. Read from each plugin's
136
+ # `#protocol_contracts` (which the manifest backs by default
137
+ # but a plugin MAY override to fold in per-project config).
138
+ # Consumed by `Inference::MethodParameterBinder` (the
139
+ # parameter-type provision) and by contributing plugins'
140
+ # `#diagnostics_for_file` hooks (the presence + return-type
141
+ # check).
142
+ def protocol_contracts
143
+ plugins.flat_map(&:protocol_contracts)
144
+ end
145
+
146
+ # ADR-28 — the subset of `protocol_contracts` whose
147
+ # `path_glob` matches `path`. Contract globs are authored
148
+ # project-root-relative (`lib/controller/**/*.rb`); the
149
+ # analyzer may hand this method either a project-relative
150
+ # path (`rigor check` run from the project root) or an
151
+ # absolute one (run from elsewhere, or a spec tmpdir), so the
152
+ # glob is matched both directly and as a `**/`-prefixed path
153
+ # suffix. `File::FNM_PATHNAME` keeps `*` from crossing `/`;
154
+ # `File::FNM_EXTGLOB` enables `{a,b}` groups. Returns `[]` for
155
+ # a nil path so the binder can call this unconditionally.
156
+ def contracts_for_path(path)
157
+ return [] if path.nil?
158
+
159
+ path_s = path.to_s
160
+ protocol_contracts.select { |contract| path_matches_glob?(contract.path_glob, path_s) }
161
+ end
162
+
163
+ FNMATCH_FLAGS = File::FNM_PATHNAME | File::FNM_EXTGLOB
164
+ private_constant :FNMATCH_FLAGS
165
+
107
166
  EMPTY = new.freeze
167
+
168
+ private
169
+
170
+ def path_matches_glob?(glob, path)
171
+ File.fnmatch?(glob, path, FNMATCH_FLAGS) ||
172
+ File.fnmatch?(File.join("**", glob), path, FNMATCH_FLAGS)
173
+ end
108
174
  end
109
175
  end
110
176
  end
@@ -122,8 +122,8 @@ module Rigor
122
122
  diagnostic_count: matched.size,
123
123
  summary: "undefined-method on core classes (#{top_methods(matched)}) — " \
124
124
  "ActiveSupport monkey-patches these",
125
- action: "Wire the rigor-activesupport-core-ext RBS bundle via " \
126
- "`signature_paths:` in .rigor.yml."
125
+ action: "Add rigor-activesupport-core-ext to `plugins:` in .rigor.yml " \
126
+ "(it is an RBS-bundle plugin — ADR-25)."
127
127
  ), matched]
128
128
  end
129
129
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require_relative "../trinary"
4
5
 
5
6
  module Rigor
@@ -24,6 +25,13 @@ module Rigor
24
25
  Complex,
25
26
  Regexp,
26
27
  Pathname,
28
+ ::Set,
29
+ # `Date` covers `DateTime` (a subclass). `Time` is core.
30
+ # Both arise only from deterministic constructor folding
31
+ # (`Date.new` / `Time.utc`) — there is no Date / Time
32
+ # literal node — so a `Constant` carrier is always sound.
33
+ Date,
34
+ Time,
27
35
  TrueClass,
28
36
  FalseClass,
29
37
  NilClass
@@ -42,12 +50,31 @@ module Rigor
42
50
  raise ArgumentError, "Rigor::Type::Constant only carries scalar literals; got #{value.class}"
43
51
  end
44
52
 
45
- @value = value.is_a?(String) ? value.dup.freeze : value
53
+ @value = freezable_carrier?(value) ? value.dup.freeze : value
46
54
  freeze
47
55
  end
48
56
 
57
+ # Mutable-ish carriers are stored as a frozen copy so a later
58
+ # in-place mutation cannot rewrite the literal under us. `Time`
59
+ # joins `String` / `Set` here: `Time#localtime` mutates the
60
+ # receiver's zone in place, so the carrier holds a frozen copy
61
+ # (the catalog also blocklists the mutators). `Date` is already
62
+ # immutable, but is duped-and-frozen for symmetry.
63
+ def freezable_carrier?(value)
64
+ value.is_a?(String) || value.is_a?(::Set) ||
65
+ value.is_a?(Date) || value.is_a?(Time)
66
+ end
67
+
68
+ # `Date#inspect` / `DateTime#inspect` spell out the internal
69
+ # astronomical-Julian-day representation, which is unreadable
70
+ # in a diagnostic. ISO-8601 is the compact, deterministic
71
+ # form. `Time#inspect` is already compact (`2026-01-01
72
+ # 00:00:00 UTC`), so it keeps the default.
49
73
  def describe(_verbosity = :short)
50
- value.inspect
74
+ case value
75
+ when Date then value.iso8601
76
+ else value.inspect
77
+ end
51
78
  end
52
79
 
53
80
  # RBS supports `Literal` types for booleans, nil, integer
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
@@ -261,10 +261,12 @@ files:
261
261
  - lib/rigor/cache/rbs_known_class_names.rb
262
262
  - lib/rigor/cache/store.rb
263
263
  - lib/rigor/cli.rb
264
+ - lib/rigor/cli/annotate_command.rb
264
265
  - lib/rigor/cli/baseline_command.rb
265
266
  - lib/rigor/cli/diff_command.rb
266
267
  - lib/rigor/cli/explain_command.rb
267
268
  - lib/rigor/cli/lsp_command.rb
269
+ - lib/rigor/cli/prism_colorizer.rb
268
270
  - lib/rigor/cli/sig_gen_command.rb
269
271
  - lib/rigor/cli/triage_command.rb
270
272
  - lib/rigor/cli/triage_renderer.rb
@@ -327,16 +329,23 @@ files:
327
329
  - lib/rigor/inference/macro_block_self_type.rb
328
330
  - lib/rigor/inference/method_dispatcher.rb
329
331
  - lib/rigor/inference/method_dispatcher/block_folding.rb
332
+ - lib/rigor/inference/method_dispatcher/cgi_folding.rb
330
333
  - lib/rigor/inference/method_dispatcher/constant_folding.rb
331
334
  - lib/rigor/inference/method_dispatcher/file_folding.rb
332
335
  - lib/rigor/inference/method_dispatcher/iterator_dispatch.rb
333
336
  - lib/rigor/inference/method_dispatcher/kernel_dispatch.rb
334
337
  - lib/rigor/inference/method_dispatcher/literal_string_folding.rb
338
+ - lib/rigor/inference/method_dispatcher/math_folding.rb
335
339
  - lib/rigor/inference/method_dispatcher/method_folding.rb
336
340
  - lib/rigor/inference/method_dispatcher/overload_selector.rb
337
341
  - lib/rigor/inference/method_dispatcher/rbs_dispatch.rb
338
342
  - lib/rigor/inference/method_dispatcher/receiver_affinity.rb
343
+ - lib/rigor/inference/method_dispatcher/regexp_folding.rb
344
+ - lib/rigor/inference/method_dispatcher/set_folding.rb
339
345
  - lib/rigor/inference/method_dispatcher/shape_dispatch.rb
346
+ - lib/rigor/inference/method_dispatcher/shellwords_folding.rb
347
+ - lib/rigor/inference/method_dispatcher/time_folding.rb
348
+ - lib/rigor/inference/method_dispatcher/uri_folding.rb
340
349
  - lib/rigor/inference/method_parameter_binder.rb
341
350
  - lib/rigor/inference/multi_target_binder.rb
342
351
  - lib/rigor/inference/narrowing.rb
@@ -378,6 +387,7 @@ files:
378
387
  - lib/rigor/plugin/macro/heredoc_template.rb
379
388
  - lib/rigor/plugin/macro/trait_registry.rb
380
389
  - lib/rigor/plugin/manifest.rb
390
+ - lib/rigor/plugin/protocol_contract.rb
381
391
  - lib/rigor/plugin/registry.rb
382
392
  - lib/rigor/plugin/services.rb
383
393
  - lib/rigor/plugin/trust_policy.rb