rigortype 0.1.15 → 0.1.17
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 +4 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
- data/lib/rigor/analysis/check_rules.rb +174 -71
- data/lib/rigor/analysis/dependency_recorder.rb +122 -0
- data/lib/rigor/analysis/diagnostic.rb +58 -0
- data/lib/rigor/analysis/incremental.rb +162 -0
- data/lib/rigor/analysis/incremental_session.rb +337 -0
- data/lib/rigor/analysis/rule_catalog.rb +48 -0
- data/lib/rigor/analysis/runner.rb +485 -29
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/builtins/static_return_refinements.rb +7 -1
- data/lib/rigor/cache/descriptor.rb +56 -51
- data/lib/rigor/cache/incremental_snapshot.rb +147 -0
- data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
- data/lib/rigor/cache/rbs_constant_table.rb +2 -8
- data/lib/rigor/cache/rbs_environment.rb +2 -8
- data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
- data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
- data/lib/rigor/cache/store.rb +99 -1
- data/lib/rigor/cli/annotate_command.rb +2 -7
- data/lib/rigor/cli/baseline_command.rb +2 -7
- data/lib/rigor/cli/command.rb +47 -0
- data/lib/rigor/cli/coverage_command.rb +3 -23
- data/lib/rigor/cli/coverage_renderer.rb +3 -8
- data/lib/rigor/cli/diff_command.rb +3 -7
- data/lib/rigor/cli/explain_command.rb +2 -7
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mcp_command.rb +3 -7
- data/lib/rigor/cli/options.rb +57 -0
- data/lib/rigor/cli/plugin_command.rb +3 -7
- data/lib/rigor/cli/plugins_command.rb +52 -10
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli/renderable.rb +26 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -7
- data/lib/rigor/cli/skill_command.rb +3 -7
- data/lib/rigor/cli/triage_command.rb +2 -7
- data/lib/rigor/cli/type_of_command.rb +5 -38
- data/lib/rigor/cli/type_of_renderer.rb +4 -9
- data/lib/rigor/cli/type_scan_command.rb +3 -23
- data/lib/rigor/cli/type_scan_renderer.rb +4 -9
- data/lib/rigor/cli.rb +260 -48
- data/lib/rigor/configuration/dependencies.rb +18 -1
- data/lib/rigor/configuration/severity_profile.rb +22 -3
- data/lib/rigor/configuration.rb +13 -3
- data/lib/rigor/environment/rbs_loader.rb +335 -4
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/block_parameter_binder.rb +1 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
- data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
- data/lib/rigor/inference/expression_typer.rb +149 -22
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
- data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +100 -23
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
- data/lib/rigor/inference/method_dispatcher.rb +147 -60
- data/lib/rigor/inference/narrowing.rb +202 -5
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +257 -11
- data/lib/rigor/inference/statement_evaluator.rb +110 -26
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/language_server/buffer_resolution.rb +33 -0
- data/lib/rigor/language_server/completion_provider.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
- data/lib/rigor/language_server/folding_range_provider.rb +4 -4
- data/lib/rigor/language_server/hover_provider.rb +4 -4
- data/lib/rigor/language_server/selection_range_provider.rb +4 -4
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +337 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +49 -1
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
- data/lib/rigor/rbs_extended.rb +39 -0
- data/lib/rigor/scope.rb +123 -9
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/acceptance_router.rb +19 -0
- data/lib/rigor/type/accepts_result.rb +3 -10
- data/lib/rigor/type/app.rb +3 -7
- data/lib/rigor/type/bot.rb +2 -3
- data/lib/rigor/type/bound_method.rb +5 -12
- data/lib/rigor/type/combinator.rb +23 -1
- data/lib/rigor/type/constant.rb +2 -3
- data/lib/rigor/type/data_class.rb +80 -0
- data/lib/rigor/type/data_instance.rb +100 -0
- data/lib/rigor/type/difference.rb +5 -10
- data/lib/rigor/type/dynamic.rb +5 -10
- data/lib/rigor/type/hash_shape.rb +5 -15
- data/lib/rigor/type/integer_range.rb +5 -10
- data/lib/rigor/type/intersection.rb +5 -10
- data/lib/rigor/type/nominal.rb +5 -10
- data/lib/rigor/type/refined.rb +5 -10
- data/lib/rigor/type/singleton.rb +5 -10
- data/lib/rigor/type/top.rb +2 -3
- data/lib/rigor/type/tuple.rb +5 -10
- data/lib/rigor/type/union.rb +69 -10
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/value_semantics.rb +77 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +48 -33
- 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 +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/cache.rbs +19 -0
- data/sig/rigor/inference.rbs +22 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +5 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor/type.rbs +58 -1
- data/sig/rigor.rbs +11 -1
- data/skills/rigor-plugin-author/SKILL.md +13 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- metadata +73 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -3,7 +3,37 @@ class Rigor::Plugin::Manifest::Consumption
|
|
|
3
3
|
end
|
|
4
4
|
|
|
5
5
|
class Rigor::Plugin::Manifest
|
|
6
|
-
def initialize: (id: untyped, version: untyped, ?description: untyped, ?
|
|
6
|
+
def initialize: (id: untyped, version: untyped, ?description: untyped, ?config_schema: untyped, ?produces: untyped, ?consumes: untyped, ?owns_receivers: untyped, ?open_receivers: untyped, ?type_node_resolvers: untyped, ?block_as_methods: untyped, ?heredoc_templates: untyped, ?nested_class_templates: untyped, ?trait_registries: untyped, ?external_files: untyped, ?hkt_registrations: untyped, ?hkt_definitions: untyped, ?signature_paths: untyped, ?protocol_contracts: untyped, ?source_rbs_synthesizer: untyped, ?additional_initializers: untyped) -> void
|
|
7
|
+
|
|
8
|
+
# Public attribute readers (the full `attr_reader` surface). Plugins
|
|
9
|
+
# read `manifest.id` / `manifest.protocol_contracts` etc.; declaring
|
|
10
|
+
# them keeps the contract surface resolvable so ADR-43 ancestor
|
|
11
|
+
# resolution does not surface a false `call.undefined-method` on a
|
|
12
|
+
# real reader. Identity fields are typed; the structured slots stay
|
|
13
|
+
# `untyped` per the sparse `sig/` tree.
|
|
14
|
+
def id: () -> String
|
|
15
|
+
def version: () -> String
|
|
16
|
+
def description: () -> String?
|
|
17
|
+
def config_schema: () -> untyped
|
|
18
|
+
def config_defaults: () -> Hash[String, untyped]
|
|
19
|
+
def produces: () -> untyped
|
|
20
|
+
def consumes: () -> untyped
|
|
21
|
+
def owns_receivers: () -> untyped
|
|
22
|
+
def open_receivers: () -> untyped
|
|
23
|
+
def type_node_resolvers: () -> untyped
|
|
24
|
+
def block_as_methods: () -> untyped
|
|
25
|
+
def heredoc_templates: () -> untyped
|
|
26
|
+
def nested_class_templates: () -> untyped
|
|
27
|
+
def trait_registries: () -> untyped
|
|
28
|
+
def external_files: () -> untyped
|
|
29
|
+
def hkt_registrations: () -> untyped
|
|
30
|
+
def hkt_definitions: () -> untyped
|
|
31
|
+
def signature_paths: () -> untyped
|
|
32
|
+
def protocol_contracts: () -> untyped
|
|
33
|
+
def source_rbs_synthesizer: () -> untyped
|
|
34
|
+
def additional_initializers: () -> untyped
|
|
35
|
+
|
|
7
36
|
def validate_config: (untyped) -> Array[String]
|
|
8
37
|
def to_h: () -> Hash[String, untyped]
|
|
38
|
+
def hash: () -> Integer
|
|
9
39
|
end
|
data/sig/rigor/rbs_extended.rbs
CHANGED
data/sig/rigor/scope.rbs
CHANGED
|
@@ -19,6 +19,8 @@ module Rigor
|
|
|
19
19
|
attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
|
|
20
20
|
attr_reader discovered_superclasses: Hash[String, String]
|
|
21
21
|
attr_reader discovered_includes: Hash[String, Array[String]]
|
|
22
|
+
attr_reader discovered_class_sources: Hash[String, Set[String]]
|
|
23
|
+
attr_reader data_member_layouts: Hash[String, Array[Symbol]]
|
|
22
24
|
attr_reader indexed_narrowings: Hash[IndexedKey, Type::t]
|
|
23
25
|
attr_reader method_chain_narrowings: Hash[ChainKey, Type::t]
|
|
24
26
|
attr_reader source_path: String?
|
|
@@ -66,8 +68,11 @@ module Rigor
|
|
|
66
68
|
def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
|
|
67
69
|
def superclass_of: (String | Symbol class_name) -> String?
|
|
68
70
|
def with_discovered_superclasses: (Hash[String, String] table) -> Scope
|
|
71
|
+
def data_member_layout: (String | Symbol class_name) -> Array[Symbol]?
|
|
72
|
+
def with_data_member_layouts: (Hash[String, Array[Symbol]] table) -> Scope
|
|
69
73
|
def includes_of: (String | Symbol class_name) -> Array[String]
|
|
70
74
|
def with_discovered_includes: (Hash[String, Array[String]] table) -> Scope
|
|
75
|
+
def with_discovered_class_sources: (Hash[String, Set[String]] table) -> Scope
|
|
71
76
|
def indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Type::t?
|
|
72
77
|
def with_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key, Type::t type) -> Scope
|
|
73
78
|
def without_indexed_narrowing: (Symbol receiver_kind, String | Symbol receiver_name, untyped key) -> Scope
|
data/sig/rigor/source.rbs
CHANGED
|
@@ -11,10 +11,22 @@ module Rigor
|
|
|
11
11
|
def at_offset: (Integer offset) -> untyped?
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
module Literals
|
|
15
|
+
def self?.symbol_or_string: (untyped? node) -> Symbol?
|
|
16
|
+
def self?.symbol_or_string_name: (untyped? node) -> String?
|
|
17
|
+
def self?.symbol: (untyped? node) -> Symbol?
|
|
18
|
+
def self?.symbol_name: (untyped? node) -> String?
|
|
19
|
+
def self?.symbol_arguments: (untyped? call_node) -> Array[Symbol]
|
|
20
|
+
def self?.symbol_arg: (untyped? call_node, Integer index) -> Symbol?
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
module NodeWalker
|
|
15
24
|
def self?.each: (untyped root) { (untyped) -> void } -> nil
|
|
16
25
|
| (untyped root) -> Enumerator[untyped, nil]
|
|
17
26
|
def self?.walk: (untyped node) { (untyped) -> void } -> void
|
|
27
|
+
def self?.each_with_ancestors: (untyped root) { (untyped, Array[untyped]) -> void } -> nil
|
|
28
|
+
| (untyped root) -> Enumerator[untyped, nil]
|
|
29
|
+
def self?.walk_with_ancestors: (untyped node, Array[untyped] ancestors) { (untyped, Array[untyped]) -> void } -> void
|
|
18
30
|
end
|
|
19
31
|
end
|
|
20
32
|
end
|
data/sig/rigor/type.rbs
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
module Rigor
|
|
2
2
|
module Type
|
|
3
|
-
type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape
|
|
3
|
+
type t = Top | Bot | Dynamic | Constant | IntegerRange | Nominal | Singleton | Union | Difference | Tuple | HashShape | DataClass | DataInstance
|
|
4
4
|
|
|
5
5
|
type accepts_mode = :strict | :gradual | :loose
|
|
6
6
|
|
|
7
|
+
# The structural interface every `Type::*` carrier satisfies — the
|
|
8
|
+
# internal type-object API (see
|
|
9
|
+
# docs/internal-spec/internal-type-api.md). A structural *interface*
|
|
10
|
+
# (duck-typed: satisfied by having the methods, no `implements`
|
|
11
|
+
# clause), not a "protocol" — that word is reserved for ADR-28
|
|
12
|
+
# path-scoped protocol contracts. The closed `type t` union above is
|
|
13
|
+
# what the engine threads day-to-day; this records the shared
|
|
14
|
+
# per-carrier method contract in one place. Carriers supply the
|
|
15
|
+
# identity trio via `Rigor::ValueSemantics` and `accepts` via
|
|
16
|
+
# `Rigor::Type::AcceptanceRouter`.
|
|
17
|
+
interface _Type
|
|
18
|
+
def describe: (?Symbol verbosity) -> String
|
|
19
|
+
def erase_to_rbs: () -> String
|
|
20
|
+
def top: () -> Trinary
|
|
21
|
+
def bot: () -> Trinary
|
|
22
|
+
def dynamic: () -> Trinary
|
|
23
|
+
def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
|
|
24
|
+
def ==: (untyped other) -> bool
|
|
25
|
+
def eql?: (untyped other) -> bool
|
|
26
|
+
def hash: () -> Integer
|
|
27
|
+
def inspect: () -> String
|
|
28
|
+
end
|
|
29
|
+
|
|
7
30
|
class Top
|
|
8
31
|
def self.instance: () -> Top
|
|
9
32
|
def describe: (?Symbol verbosity) -> String
|
|
@@ -222,6 +245,38 @@ module Rigor
|
|
|
222
245
|
def inspect: () -> String
|
|
223
246
|
end
|
|
224
247
|
|
|
248
|
+
class DataClass
|
|
249
|
+
attr_reader members: Array[Symbol]
|
|
250
|
+
attr_reader class_name: String?
|
|
251
|
+
def initialize: (Array[Symbol] members, ?String? class_name) -> void
|
|
252
|
+
def describe: (?Symbol verbosity) -> String
|
|
253
|
+
def erase_to_rbs: () -> String
|
|
254
|
+
def top: () -> Trinary
|
|
255
|
+
def bot: () -> Trinary
|
|
256
|
+
def dynamic: () -> Trinary
|
|
257
|
+
def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
|
|
258
|
+
def ==: (untyped other) -> bool
|
|
259
|
+
def hash: () -> Integer
|
|
260
|
+
def inspect: () -> String
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class DataInstance
|
|
264
|
+
attr_reader members: Hash[Symbol, Type::t]
|
|
265
|
+
attr_reader class_name: String?
|
|
266
|
+
def initialize: (Hash[Symbol, Type::t] members, ?String? class_name) -> void
|
|
267
|
+
def member_names: () -> Array[Symbol]
|
|
268
|
+
def member_type: (Symbol name) -> Type::t?
|
|
269
|
+
def describe: (?Symbol verbosity) -> String
|
|
270
|
+
def erase_to_rbs: () -> String
|
|
271
|
+
def top: () -> Trinary
|
|
272
|
+
def bot: () -> Trinary
|
|
273
|
+
def dynamic: () -> Trinary
|
|
274
|
+
def accepts: (Type::t other, ?mode: accepts_mode) -> AcceptsResult
|
|
275
|
+
def ==: (untyped other) -> bool
|
|
276
|
+
def hash: () -> Integer
|
|
277
|
+
def inspect: () -> String
|
|
278
|
+
end
|
|
279
|
+
|
|
225
280
|
class AcceptsResult
|
|
226
281
|
attr_reader trinary: Trinary
|
|
227
282
|
attr_reader mode: accepts_mode
|
|
@@ -282,6 +337,8 @@ module Rigor
|
|
|
282
337
|
def self?.universal_int: () -> IntegerRange
|
|
283
338
|
def self?.tuple_of: (*Type::t elements) -> Tuple
|
|
284
339
|
def self?.hash_shape_of: (?Hash[untyped, Type::t]? pairs, **untyped options) -> HashShape
|
|
340
|
+
def self?.data_class_of: (members: Array[Symbol], ?class_name: String?) -> DataClass
|
|
341
|
+
def self?.data_instance_of: (members: Hash[Symbol, Type::t], ?class_name: String?) -> DataInstance
|
|
285
342
|
def self?.union: (*Type::t types) -> Type::t
|
|
286
343
|
def self?.key_of: (Type::t type) -> Type::t
|
|
287
344
|
def self?.value_of: (Type::t type) -> Type::t
|
data/sig/rigor.rbs
CHANGED
|
@@ -53,6 +53,11 @@ module Rigor
|
|
|
53
53
|
def error?: () -> bool
|
|
54
54
|
def to_h: () -> Hash[String, untyped]
|
|
55
55
|
def to_s: () -> String
|
|
56
|
+
|
|
57
|
+
# Singleton factories plugins build diagnostics through (the
|
|
58
|
+
# `node` / `location` are Prism values, received as `untyped`).
|
|
59
|
+
def self.from_node: (untyped node, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
60
|
+
def self.from_location: (untyped location, path: String, message: String, ?severity: Symbol, ?rule: String?, ?source_family: String, ?receiver_type: untyped, ?method_name: untyped, ?project_definition_site: untyped) -> Rigor::Analysis::Diagnostic
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
class Result
|
|
@@ -108,8 +113,13 @@ module Rigor
|
|
|
108
113
|
attr_reader cache_store: untyped
|
|
109
114
|
attr_reader plugin_registry: untyped
|
|
110
115
|
attr_reader buffer: untyped
|
|
111
|
-
|
|
116
|
+
attr_reader file_dependencies: Hash[String, untyped]
|
|
117
|
+
attr_reader analyzed_files: Array[String]
|
|
118
|
+
def initialize: (configuration: Configuration, ?explain: bool, ?cache_store: untyped, ?plugin_requirer: untyped, ?workers: Integer, ?collect_stats: bool, ?buffer: untyped, ?prebuilt: untyped, ?environment: untyped, ?record_dependencies: bool, ?analyze_only: untyped) -> void
|
|
112
119
|
def run: (?Array[String] paths) -> Result
|
|
120
|
+
def run_source: (source: String, ?path: String) -> Result
|
|
121
|
+
def analysis_file_set: (?Array[String] paths) -> Array[String]
|
|
122
|
+
def file_dependents: () -> Hash[String, untyped]
|
|
113
123
|
def prepare_project_scan: (?paths: Array[String]) -> untyped
|
|
114
124
|
end
|
|
115
125
|
end
|
|
@@ -90,20 +90,24 @@ Do NOT use this skill for:
|
|
|
90
90
|
A Rigor plugin is a Ruby class that subclasses `Rigor::Plugin::Base`,
|
|
91
91
|
declares a `manifest(id:, version:, …)`, and calls
|
|
92
92
|
`Rigor::Plugin.register(self)` at load time. When `.rigor.yml` lists
|
|
93
|
-
the plugin under `plugins:`, Rigor `require`s it and
|
|
94
|
-
|
|
95
|
-
scope
|
|
96
|
-
|
|
97
|
-
returns an array of
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
the plugin under `plugins:`, Rigor `require`s it and runs its
|
|
94
|
+
**node rules**: the plugin declares `node_rule(Prism::CallNode) { |node,
|
|
95
|
+
scope, path, _fc, context| … }`, and the engine — which owns the single
|
|
96
|
+
AST walk per file — hands every matching node to the block along with a
|
|
97
|
+
`scope` it can query for inferred types. The block returns an array of
|
|
98
|
+
`Rigor::Analysis::Diagnostic` (built via the `diagnostic` helper).
|
|
99
|
+
Optionally the plugin also declares `dynamic_return(receivers:)` /
|
|
100
|
+
`type_specifier(methods:)` to *supply* a return type or narrowing facts
|
|
101
|
+
for call sites the core analyzer types as `Dynamic`. (The older
|
|
102
|
+
`#diagnostics_for_file` / `#flow_contribution_for` fat hooks remain as
|
|
103
|
+
deprecated escape valves — see Phase 2.)
|
|
100
104
|
|
|
101
105
|
## Phase outline
|
|
102
106
|
|
|
103
107
|
| Phase | What | Reference |
|
|
104
108
|
| --- | --- | --- |
|
|
105
109
|
| 1 | Package and scaffold — gem vs project-private layout, gemspec / Gemfile, the plugin class skeleton, `.rigor.yml` activation. | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) |
|
|
106
|
-
| 2 |
|
|
110
|
+
| 2 | Node rules — `node_rule` (engine-owned walk), building `Diagnostic`s via `Base#diagnostic`, querying `scope.type_of`, calling the target library directly instead of reimplementing it (ADR-39: `Plugin::Inflector`, `Base.suggest`), optional `dynamic_return` / `type_specifier`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
|
|
107
111
|
| 3 | Test and ship — fixture-based tests (RSpec / Minitest, no rigor internals), version pinning, README, publish or keep private. | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) |
|
|
108
112
|
|
|
109
113
|
## Reading order — modules
|
|
@@ -111,5 +115,5 @@ return type for call sites the core analyzer types as `Dynamic`.
|
|
|
111
115
|
| Module | Read | Covers |
|
|
112
116
|
| --- | --- | --- |
|
|
113
117
|
| 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
|
|
114
|
-
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `
|
|
118
|
+
| 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (and the deprecated `flow_contribution_for` escape valve), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
|
|
115
119
|
| 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
|
|
@@ -184,10 +184,11 @@ module Rigor
|
|
|
184
184
|
@module_name = config.fetch("module_name", "Default")
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
-
#
|
|
188
|
-
# `scope` answers inferred-type queries
|
|
189
|
-
# Rigor::Analysis::Diagnostic
|
|
190
|
-
|
|
187
|
+
# The engine owns the AST walk and hands every matching node to
|
|
188
|
+
# this rule. `scope` answers inferred-type queries; return an
|
|
189
|
+
# Array of Rigor::Analysis::Diagnostic (built via `diagnostic`).
|
|
190
|
+
# See 02-walker-and-types.md.
|
|
191
|
+
node_rule Prism::CallNode do |node, scope, path, _file_context, _context|
|
|
191
192
|
[]
|
|
192
193
|
end
|
|
193
194
|
end
|
|
@@ -227,7 +228,7 @@ inert.
|
|
|
227
228
|
|
|
228
229
|
## Output of this module
|
|
229
230
|
|
|
230
|
-
A scaffolded plugin that loads (even if `
|
|
231
|
+
A scaffolded plugin that loads (even if its `node_rule` still
|
|
231
232
|
returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
|
|
232
233
|
([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
|
|
233
234
|
actually analyse.
|
|
@@ -1,57 +1,94 @@
|
|
|
1
|
-
# 02 —
|
|
1
|
+
# 02 — Node rules and types
|
|
2
2
|
|
|
3
|
-
Covers **Phase 2** —
|
|
4
|
-
|
|
3
|
+
Covers **Phase 2** — recognising the DSL's call shapes, emitting
|
|
4
|
+
diagnostics, and (optionally) contributing return types. The engine
|
|
5
|
+
owns the AST walk; you write the per-node check.
|
|
5
6
|
|
|
6
|
-
## `
|
|
7
|
+
## `node_rule` — the core hook (ADR-37)
|
|
8
|
+
|
|
9
|
+
A diagnostic-emitting plugin declares a **node rule**. The engine walks
|
|
10
|
+
each analysed file's AST **once** and hands every node of the declared
|
|
11
|
+
type to your block — so you write the check and never the traversal:
|
|
7
12
|
|
|
8
13
|
```ruby
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
class MyPlugin < Rigor::Plugin::Base
|
|
15
|
+
manifest(id: "my-plugin", version: "0.1.0")
|
|
16
|
+
|
|
17
|
+
node_rule Prism::CallNode do |node, scope, path, file_context, context|
|
|
18
|
+
# node — the Prism::CallNode the engine matched.
|
|
19
|
+
# scope — answers inferred-type queries: scope.type_of(node).
|
|
20
|
+
# path — the file path, for the diagnostic location.
|
|
21
|
+
# file_context — the value node_file_context built (see below); nil if none.
|
|
22
|
+
# context — Rigor::Plugin::NodeContext: lexical ancestors.
|
|
23
|
+
# → return Array<Rigor::Analysis::Diagnostic> ([] to fire nothing)
|
|
24
|
+
end
|
|
14
25
|
end
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
the
|
|
19
|
-
|
|
20
|
-
`
|
|
28
|
+
`node_rule(node_type)` matches by `node.is_a?(node_type)`, so declare
|
|
29
|
+
the concrete class you care about (`Prism::CallNode` for almost every
|
|
30
|
+
DSL plugin) or `Prism::Node` to see everything. The block runs through
|
|
31
|
+
`instance_exec`, so `config`, `services`, `services.fact_store`, and the
|
|
32
|
+
`diagnostic` helper (below) are all in scope. Declare multiple rules
|
|
33
|
+
(they run in declaration order); a plugin that declares none pays zero
|
|
34
|
+
cost. Keep the matching logic in a separate `Analyzer` class once it
|
|
35
|
+
grows — have it take the call node and return location-free `Violation`
|
|
36
|
+
rows the rule positions with `diagnostic` (the bundled plugins follow
|
|
37
|
+
this `Analyzer.violations_for` split, which also makes the logic
|
|
38
|
+
unit-testable without running the whole engine).
|
|
39
|
+
|
|
40
|
+
> The legacy `diagnostics_for_file(path:, scope:, root:)` hook — where
|
|
41
|
+
> you hand-rolled a `def walk` / `compact_child_nodes.each` recursion
|
|
42
|
+
> over `root` yourself — is the **deprecated whole-file escape valve**.
|
|
43
|
+
> Use it only for a genuinely file-scoped result (a single load-error
|
|
44
|
+
> row, or a check that needs the whole parsed file at once). New
|
|
45
|
+
> node-scoped checks use `node_rule`.
|
|
21
46
|
|
|
22
47
|
### Recognising call sites
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
49
|
+
Inside the rule, match on `node.name` (the method name, a Symbol),
|
|
50
|
+
`node.receiver` (nil for an implicit-self call), and
|
|
51
|
+
`node.arguments&.arguments`. For literal `:sym` / `"str"` arguments
|
|
52
|
+
reach for `Rigor::Source::Literals` (`symbol_or_string(node)` /
|
|
53
|
+
`symbol_arguments(call)` / `symbol_arg(call, i)`) rather than
|
|
54
|
+
re-deriving the `unescaped.to_sym` shape.
|
|
55
|
+
|
|
56
|
+
### Two-pass and lexical context
|
|
57
|
+
|
|
58
|
+
- **Same-file collect-then-validate** (gather declared names, then
|
|
59
|
+
validate references): declare `node_file_context { |root, scope| … }`.
|
|
60
|
+
It runs once per file before the walk and its return value is threaded
|
|
61
|
+
to every rule as `file_context`. (A *cross-file* collect belongs in
|
|
62
|
+
`#prepare` + `services.fact_store` instead — see
|
|
63
|
+
[`01-plan-and-scaffold.md`](01-plan-and-scaffold.md).)
|
|
64
|
+
- **Where the node sits**: `context` (a `Rigor::Plugin::NodeContext`)
|
|
65
|
+
carries the lexical ancestor chain — `context.enclosing_def`,
|
|
66
|
+
`context.enclosing_module`, `context.enclosing_block(:describe)`, and
|
|
67
|
+
the raw `context.ancestors`. Read it when the check depends on the
|
|
68
|
+
enclosing class / method / block DSL (the enclosing controller a
|
|
69
|
+
`before_action` sits in, the `describe <Model>` a matcher is under).
|
|
35
70
|
|
|
36
71
|
## Building a `Diagnostic`
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
Use the `Base#diagnostic` helper — it internalises the load-bearing
|
|
74
|
+
1-based `line` / `start_column + 1` convention so you never unpack
|
|
75
|
+
`node.location` by hand:
|
|
40
76
|
|
|
41
77
|
```ruby
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
line: loc.start_line,
|
|
47
|
-
column: loc.start_column + 1, # 1-based column
|
|
48
|
-
message: message,
|
|
49
|
-
severity: severity, # :error | :warning | :info
|
|
50
|
-
rule: rule # short kebab-case id
|
|
51
|
-
)
|
|
78
|
+
node_rule Prism::CallNode do |node, scope, path, _fc, _ctx|
|
|
79
|
+
next [] unless offending?(node)
|
|
80
|
+
|
|
81
|
+
[diagnostic(node, path: path, message: "...", severity: :error, rule: "my-rule")]
|
|
52
82
|
end
|
|
53
83
|
```
|
|
54
84
|
|
|
85
|
+
Pass `location:` (a Prism location) to point at a sub-location rather
|
|
86
|
+
than the whole node — typically `location: node.message_loc`, so a
|
|
87
|
+
method-name diagnostic points at the name, not the receiver-spanning
|
|
88
|
+
whole call. Do **not** set `source_family` — the runner stamps
|
|
89
|
+
`plugin.<manifest.id>` on every returned diagnostic, so anything you set
|
|
90
|
+
is overwritten.
|
|
91
|
+
|
|
55
92
|
`rule` is a short identifier (`dimension-mismatch`,
|
|
56
93
|
`unknown-state`). Rigor namespaces it under your plugin —
|
|
57
94
|
diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
|
|
@@ -87,46 +124,80 @@ Match with `case`/`when` on the carrier class. Treat any carrier you
|
|
|
87
124
|
do not recognise as "decline to act" — never crash on an unexpected
|
|
88
125
|
type.
|
|
89
126
|
|
|
90
|
-
##
|
|
127
|
+
## Use the target library, don't reimplement it (ADR-39)
|
|
128
|
+
|
|
129
|
+
A plugin may **call the pure methods of the library it targets
|
|
130
|
+
directly** instead of reimplementing them — the Ruby analogue of a
|
|
131
|
+
PHPStan extension calling into the real framework. Reimplementing a
|
|
132
|
+
library's rules (an inflector, a status-code table, …) risks diverging
|
|
133
|
+
from the real behaviour, and a wrong *derived* fact (a wrong class /
|
|
134
|
+
helper / column name) is a false positive on working code. So declare
|
|
135
|
+
the target gem as your plugin's dependency and call its safe methods.
|
|
91
136
|
|
|
92
|
-
|
|
137
|
+
The bundled `Rigor::Plugin::Inflector` is the worked example — it calls
|
|
138
|
+
the real `ActiveSupport::Inflector` rather than a hand-rolled
|
|
139
|
+
approximation. If your plugin needs inflection, use it:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
Rigor::Plugin::Inflector.classify("users") # => "User"
|
|
143
|
+
Rigor::Plugin::Inflector.tableize("BlogPost") # => "blog_posts"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For your own target library, follow the same harness: a **fixed
|
|
147
|
+
allow-list** of pure methods, inputs derived from source, and **decline
|
|
148
|
+
(return nil / emit nothing) when the library is unavailable — never
|
|
149
|
+
approximate**. How the call is isolated from Rigor (in-process, a forked
|
|
150
|
+
worker, or a `Ruby::Box`) is a configurable strategy the user picks
|
|
151
|
+
(`plugins_isolation:`); you just call the method.
|
|
152
|
+
|
|
153
|
+
For the common "did you mean …?" suggestion, use the shared helper
|
|
154
|
+
rather than hand-rolling Levenshtein:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
Rigor::Plugin::Base.suggest(typo, known_names) # nearest match, or nil
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Optional — contribute a return type with `dynamic_return` / `type_specifier`
|
|
161
|
+
|
|
162
|
+
> **Critical — these hooks do NOT make a method "defined", so they do
|
|
93
163
|
> NOT suppress `call.undefined-method`.** Method *existence* and call
|
|
94
|
-
> *type* are two independent checks.
|
|
164
|
+
> *type* are two independent checks. A return-type contribution sharpens
|
|
95
165
|
> the type of a call the analyzer has **already resolved to a real
|
|
96
166
|
> method** (turning a `Dynamic` return into something precise). It is
|
|
97
167
|
> never consulted for a receiver/method the analyzer cannot find — that
|
|
98
|
-
> fires `call.undefined-method` first, and a
|
|
99
|
-
>
|
|
100
|
-
>
|
|
101
|
-
>
|
|
102
|
-
>
|
|
103
|
-
>
|
|
104
|
-
>
|
|
105
|
-
>
|
|
168
|
+
> fires `call.undefined-method` first, and a contribution does nothing
|
|
169
|
+
> to silence it. **If your goal is to kill a `call.undefined-method`
|
|
170
|
+
> cluster on a DSL-generated method (the common reason
|
|
171
|
+
> `rigor-project-init` hands off to this skill), the fix is to make the
|
|
172
|
+
> method *exist* in Rigor's view — ship RBS declaring it (see "Shipping
|
|
173
|
+
> RBS for the DSL" below), not a return-type contribution.** Reach for
|
|
174
|
+
> these only when the call already resolves and you want a *better
|
|
175
|
+
> return type*.
|
|
106
176
|
|
|
107
177
|
A plugin can do more than emit diagnostics: it can *supply* the
|
|
108
|
-
inferred return type for a call site the core
|
|
109
|
-
otherwise type as `Dynamic`.
|
|
178
|
+
inferred return type (or narrowing facts) for a call site the core
|
|
179
|
+
analyzer would otherwise type as `Dynamic`. ADR-37 gives two narrow,
|
|
180
|
+
declaratively-gated DSLs — prefer them; the engine indexes plugins by
|
|
181
|
+
their gate so it only calls the block for matching calls:
|
|
110
182
|
|
|
111
183
|
```ruby
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Rigor::
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
184
|
+
# Per-call-site RETURN TYPE, gated on the receiver's class.
|
|
185
|
+
# The block fires only when the receiver type's class is (or inherits
|
|
186
|
+
# from) a declared `receivers:` entry. Return a Rigor::Type or nil.
|
|
187
|
+
dynamic_return receivers: ["Money"] do |call_node, scope|
|
|
188
|
+
next nil unless call_node.name == :+
|
|
189
|
+
Rigor::Type::Combinator.nominal_of("Money")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Post-return NARROWING FACTS, gated on the call's method name.
|
|
193
|
+
# Return an Array of facts (or nil). Used for assertion / predicate
|
|
194
|
+
# narrowing (`assert_kind_of(Foo, x)` ⇒ x is Foo afterwards).
|
|
195
|
+
type_specifier methods: [:assert_kind_of] do |call_node, scope|
|
|
196
|
+
# ... build and return the post-return facts ...
|
|
126
197
|
end
|
|
127
198
|
```
|
|
128
199
|
|
|
129
|
-
Build
|
|
200
|
+
Build return types with `Rigor::Type::Combinator`:
|
|
130
201
|
|
|
131
202
|
```ruby
|
|
132
203
|
Rigor::Type::Combinator.nominal_of("Money") # a class type
|
|
@@ -134,14 +205,27 @@ Rigor::Type::Combinator.constant_of(true) # a literal
|
|
|
134
205
|
Rigor::Type::Combinator.union(a, b) # a union
|
|
135
206
|
```
|
|
136
207
|
|
|
137
|
-
Returning `nil` is always safe — it means "no contribution",
|
|
138
|
-
core analyzer keeps its own answer. Contribute
|
|
208
|
+
Returning `nil` (or `[]`) is always safe — it means "no contribution",
|
|
209
|
+
and the core analyzer keeps its own answer. Contribute only when the
|
|
139
210
|
plugin is confident; a wrong contribution propagates downstream.
|
|
140
211
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
`receivers:` / `methods:` are the greppable gates the
|
|
213
|
+
`rigor plugins --capabilities` catalogue enumerates — run it to see
|
|
214
|
+
exactly what each loaded plugin contributes.
|
|
215
|
+
|
|
216
|
+
> **The deprecated escape valve.** The original fat hook,
|
|
217
|
+
> `flow_contribution_for(call_node:, scope:)`, returns a single
|
|
218
|
+
> `Rigor::FlowContribution` and is still consulted alongside the narrow
|
|
219
|
+
> DSLs. It is retained only for the two shapes the narrow DSLs do not
|
|
220
|
+
> express: a **method-gated return type** (an RSpec `let(:x) { … }`
|
|
221
|
+
> binding, a Sorbet `sig`-driven return — keyed on the method, not a
|
|
222
|
+
> fixed receiver class) and a **dynamic per-project receiver set**
|
|
223
|
+
> (ActiveStorage's `Attached::One` on discovered model classes). If your
|
|
224
|
+
> plugin needs one of those, use `flow_contribution_for`; otherwise
|
|
225
|
+
> prefer `dynamic_return` / `type_specifier`. These return-type surfaces
|
|
226
|
+
> are the most contract-sensitive part of the API — implement one only
|
|
227
|
+
> if the plugin genuinely needs to sharpen call-site types; a
|
|
228
|
+
> diagnostics-only plugin skips them entirely.
|
|
145
229
|
|
|
146
230
|
## Shipping RBS for the DSL — the way to suppress `call.undefined-method`
|
|
147
231
|
|
|
@@ -151,7 +235,7 @@ If the DSL introduces methods or classes that Rigor cannot see (a
|
|
|
151
235
|
Rigor RBS declaring them so *core* inference — not just your plugin —
|
|
152
236
|
treats them as **defined**. This is what removes the
|
|
153
237
|
`call.undefined-method` diagnostics on those methods; nothing else
|
|
154
|
-
(not `
|
|
238
|
+
(not a `node_rule`, not `dynamic_return` / `type_specifier`) makes a
|
|
155
239
|
method exist in Rigor's view.
|
|
156
240
|
|
|
157
241
|
Two ways to wire the RBS, depending on how the plugin is packaged:
|
|
@@ -197,8 +281,8 @@ They compose — many plugins ship both.
|
|
|
197
281
|
|
|
198
282
|
## Output of this module
|
|
199
283
|
|
|
200
|
-
A plugin whose `
|
|
201
|
-
|
|
202
|
-
`
|
|
203
|
-
|
|
284
|
+
A plugin whose `node_rule`(s) recognise the DSL and emit diagnostics
|
|
285
|
+
with correct severities and rule ids — optionally a `dynamic_return` /
|
|
286
|
+
`type_specifier` and a `sig/` bundle. Verify by eye with `rigor check`;
|
|
287
|
+
lock it down with tests in Phase 3
|
|
204
288
|
([`03-test-and-ship.md`](03-test-and-ship.md)).
|
|
@@ -114,9 +114,9 @@ SKILL.md). Concretely:
|
|
|
114
114
|
- Your plugin's own version is normal semver, independent of
|
|
115
115
|
`rigortype`'s.
|
|
116
116
|
- When you bump the `rigortype` pin to a new minor, **re-run the
|
|
117
|
-
full test suite** — the
|
|
118
|
-
type-carrier shapes may have changed. The
|
|
119
|
-
what catch a contract drift.
|
|
117
|
+
full test suite** — the `node_rule` / contribution hook signatures
|
|
118
|
+
or the `Diagnostic` / type-carrier shapes may have changed. The
|
|
119
|
+
fixture CLI tests are what catch a contract drift.
|
|
120
120
|
- State the supported `rigortype` range in the README so users do
|
|
121
121
|
not pair the plugin with an incompatible Rigor.
|
|
122
122
|
|