rigortype 0.1.9 → 0.1.11
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 +1 -1
- data/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +57 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli.rb +88 -5
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +159 -4
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +143 -12
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +115 -7
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +26 -2
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +28 -5
- data/lib/rigor/plugin/manifest.rb +33 -5
- data/lib/rigor/plugin/registry.rb +21 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +178 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +310 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +76 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +177 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +589 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +150 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +123 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +247 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +273 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +240 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +94 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +514 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +34 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +463 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +277 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +167 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +161 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +103 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +490 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +158 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +180 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "rigor/plugin"
|
|
5
|
+
|
|
6
|
+
require_relative "hanami/action_checker"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Plugin
|
|
10
|
+
# rigor-hanami — enforces the Hanami::Action protocol.
|
|
11
|
+
#
|
|
12
|
+
# Hanami 3.x actions inherit from `Hanami::Action` and
|
|
13
|
+
# define a single entry-point:
|
|
14
|
+
#
|
|
15
|
+
# def handle(request, response)
|
|
16
|
+
# response.status = 200
|
|
17
|
+
# response.body = "Hello"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# The method is void — the response is mutated in-place;
|
|
21
|
+
# the return value is discarded by the framework.
|
|
22
|
+
#
|
|
23
|
+
# This plugin uses the **ADR-28 path-scoped
|
|
24
|
+
# method-protocol contract** to:
|
|
25
|
+
#
|
|
26
|
+
# 1. **Provide** `Hanami::Action::Request` and
|
|
27
|
+
# `Hanami::Action::Response` as the types of the
|
|
28
|
+
# first and second parameters of every `#handle`
|
|
29
|
+
# method defined inside `app/actions/**/*.rb`.
|
|
30
|
+
# The engine substitutes these types for the usual
|
|
31
|
+
# `Dynamic[Top]` fallback, so misuse of `request` or
|
|
32
|
+
# `response` inside an action body surfaces as a
|
|
33
|
+
# core diagnostic.
|
|
34
|
+
#
|
|
35
|
+
# 2. **Check** that every class under `app/actions/`
|
|
36
|
+
# defines `#handle`. Missing definitions emit
|
|
37
|
+
# `missing-handle-method`.
|
|
38
|
+
#
|
|
39
|
+
# Return-type conformance is NOT checked — Hanami
|
|
40
|
+
# actions are void by contract, and checking a void
|
|
41
|
+
# return would produce false positives on every `if`
|
|
42
|
+
# branch that doesn't end with an explicit `nil`.
|
|
43
|
+
#
|
|
44
|
+
# ## Configuration
|
|
45
|
+
#
|
|
46
|
+
# plugins:
|
|
47
|
+
# - gem: rigor-hanami
|
|
48
|
+
# config:
|
|
49
|
+
# action_path: "app/actions/**/*.rb" # default; optional
|
|
50
|
+
#
|
|
51
|
+
# ## Diagnostics
|
|
52
|
+
#
|
|
53
|
+
# | Event | Severity | Rule |
|
|
54
|
+
# | --- | --- | --- |
|
|
55
|
+
# | action class defines no `#handle` | `:error` | `missing-handle-method` |
|
|
56
|
+
# | `request` / `response` misuse in body | core engine diagnostic |
|
|
57
|
+
#
|
|
58
|
+
# A misused `request` or `response` (e.g.
|
|
59
|
+
# `request.no_such_method`) surfaces as a standard
|
|
60
|
+
# engine `call.undefined-method` diagnostic once the
|
|
61
|
+
# plugin provides the parameter types.
|
|
62
|
+
class Hanami < Rigor::Plugin::Base
|
|
63
|
+
manifest(
|
|
64
|
+
id: "hanami",
|
|
65
|
+
version: "0.1.0",
|
|
66
|
+
description: "Enforces the Hanami::Action protocol: #handle(request, response) → void.",
|
|
67
|
+
config_schema: {
|
|
68
|
+
"action_path" => :string
|
|
69
|
+
},
|
|
70
|
+
signature_paths: ["sig"],
|
|
71
|
+
protocol_contracts: [
|
|
72
|
+
Rigor::Plugin::ProtocolContract.new(
|
|
73
|
+
path_glob: "app/actions/**/*.rb",
|
|
74
|
+
method_name: :handle,
|
|
75
|
+
param_types: [
|
|
76
|
+
{ index: 0, type_name: "Hanami::Action::Request" },
|
|
77
|
+
{ index: 1, type_name: "Hanami::Action::Response" }
|
|
78
|
+
]
|
|
79
|
+
# return_type_name: nil — void; skip return-type conformance check
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def init(_services)
|
|
85
|
+
override = config["action_path"]
|
|
86
|
+
@protocol_contracts =
|
|
87
|
+
if override.nil? || override.to_s.empty?
|
|
88
|
+
manifest.protocol_contracts
|
|
89
|
+
else
|
|
90
|
+
manifest.protocol_contracts.map { |c| c.with_path_glob(override.to_s) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ADR-28: override so the per-project `action_path` config
|
|
95
|
+
# reaches both the engine's parameter-provision tier and
|
|
96
|
+
# this plugin's check half.
|
|
97
|
+
def protocol_contracts
|
|
98
|
+
@protocol_contracts || manifest.protocol_contracts
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
102
|
+
contracts = protocol_contracts
|
|
103
|
+
return [] if contracts.empty?
|
|
104
|
+
|
|
105
|
+
ActionChecker.new(contracts: contracts).check(path: path, root: root)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
Rigor::Plugin.register(Hanami)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Minimal Hanami::Action type stubs for rigor-hanami.
|
|
2
|
+
#
|
|
3
|
+
# Hanami::Action::Request and Hanami::Action::Response are
|
|
4
|
+
# defined as standalone types (without declaring Rack::Request /
|
|
5
|
+
# Rack::Response inheritance) so this file is self-contained
|
|
6
|
+
# regardless of whether a Rack RBS bundle is present in the
|
|
7
|
+
# analysed project.
|
|
8
|
+
#
|
|
9
|
+
# The methods listed here cover the surface documented at:
|
|
10
|
+
# https://guides.hanamirb.org/v3.0/actions/request-and-response/
|
|
11
|
+
|
|
12
|
+
module Hanami
|
|
13
|
+
module Action
|
|
14
|
+
# Represents the incoming HTTP request inside `#handle`.
|
|
15
|
+
class Request
|
|
16
|
+
def path_info: () -> String
|
|
17
|
+
def request_method: () -> String
|
|
18
|
+
def get?: () -> bool
|
|
19
|
+
def post?: () -> bool
|
|
20
|
+
def patch?: () -> bool
|
|
21
|
+
def put?: () -> bool
|
|
22
|
+
def delete?: () -> bool
|
|
23
|
+
def head?: () -> bool
|
|
24
|
+
def xhr?: () -> bool
|
|
25
|
+
def params: () -> Hanami::Action::Params
|
|
26
|
+
def session: () -> Hash[Symbol, untyped]
|
|
27
|
+
def flash: () -> Hash[Symbol, untyped]
|
|
28
|
+
def content_type: () -> String?
|
|
29
|
+
def env: () -> Hash[String, untyped]
|
|
30
|
+
def body: () -> String
|
|
31
|
+
def get_header: (String name) -> String?
|
|
32
|
+
def ip: () -> String?
|
|
33
|
+
def referer: () -> String?
|
|
34
|
+
def user_agent: () -> String?
|
|
35
|
+
def subdomain: (?Integer tld_length) -> String?
|
|
36
|
+
def subdomains: (?Integer tld_length) -> Array[String]
|
|
37
|
+
def cookies: () -> Hash[String, String]
|
|
38
|
+
def host: () -> String
|
|
39
|
+
def port: () -> Integer
|
|
40
|
+
def url: () -> String
|
|
41
|
+
def base_url: () -> String
|
|
42
|
+
def query_string: () -> String
|
|
43
|
+
def multipart?: () -> bool
|
|
44
|
+
def form_data?: () -> bool
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Represents the outgoing HTTP response inside `#handle`.
|
|
48
|
+
# The response is mutated in-place; the `#handle` return
|
|
49
|
+
# value is discarded by the framework.
|
|
50
|
+
class Response
|
|
51
|
+
def status: () -> Integer
|
|
52
|
+
def status=: (Integer | Symbol) -> void
|
|
53
|
+
def body: () -> Array[String]
|
|
54
|
+
def body=: (String | Array[String]) -> void
|
|
55
|
+
def headers: () -> Hash[String, String]
|
|
56
|
+
def format=: (Symbol | String) -> void
|
|
57
|
+
def redirect_to: (String url, ?Integer status) -> void
|
|
58
|
+
def write: (String str) -> void
|
|
59
|
+
def set_cookie: (String name, ?(String | Hash[Symbol, untyped]) value) -> void
|
|
60
|
+
def delete_cookie: (String name, ?Hash[Symbol, untyped] options) -> void
|
|
61
|
+
def content_type: () -> String
|
|
62
|
+
def content_type=: (String) -> void
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Typed parameter container. Values are coerced by the
|
|
66
|
+
# action's `params` block schema (if declared); otherwise
|
|
67
|
+
# all values are `untyped`.
|
|
68
|
+
class Params
|
|
69
|
+
def []: (Symbol key) -> untyped
|
|
70
|
+
def dig: (Symbol, *Symbol) -> untyped
|
|
71
|
+
def valid?: () -> bool
|
|
72
|
+
def errors: () -> Hash[Symbol, Array[String]]
|
|
73
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
74
|
+
def fetch: (Symbol key, ?untyped default) -> untyped
|
|
75
|
+
def slice: (*Symbol keys) -> Hash[Symbol, untyped]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "rigor/type"
|
|
5
|
+
require "rigor/flow_contribution"
|
|
6
|
+
require "rigor/flow_contribution/fact"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Plugin
|
|
10
|
+
class Minitest < Rigor::Plugin::Base
|
|
11
|
+
# Recognises the four shapes of Minitest / Test::Unit
|
|
12
|
+
# assertions and emits a `post_return_fact` that narrows
|
|
13
|
+
# the named local on the post-call edge.
|
|
14
|
+
#
|
|
15
|
+
# ## Recognised call shapes
|
|
16
|
+
#
|
|
17
|
+
# ### (1) Minitest / Test::Unit `assert_*` (positive) ###
|
|
18
|
+
#
|
|
19
|
+
# assert_kind_of(T, x) → narrow x to T
|
|
20
|
+
# assert_instance_of(T, x) → narrow x to T
|
|
21
|
+
# assert_nil(x) → narrow x to Constant<nil>
|
|
22
|
+
# assert_equal(literal, x) → narrow x to Constant<literal>
|
|
23
|
+
# assert_match(regex, x) → narrow x to String
|
|
24
|
+
#
|
|
25
|
+
# ### (2) Minitest / Test::Unit `refute_*` / `assert_not_*` (negative) ###
|
|
26
|
+
#
|
|
27
|
+
# refute_kind_of(T, x) → narrow x AWAY from T
|
|
28
|
+
# refute_instance_of(T, x) → narrow x AWAY from T
|
|
29
|
+
# refute_nil(x) → narrow x AWAY from nil
|
|
30
|
+
# refute_equal(literal, x) → narrow x AWAY from Constant<literal>
|
|
31
|
+
#
|
|
32
|
+
# Test::Unit's `assert_not_nil(x)` / `assert_not_equal(...)` /
|
|
33
|
+
# `assert_not_kind_of(...)` / `assert_not_instance_of(...)`
|
|
34
|
+
# share the recognizer with `refute_*` (they're aliases).
|
|
35
|
+
#
|
|
36
|
+
# ### (3) Minitest/spec `_(x).must_*` (positive) ###
|
|
37
|
+
#
|
|
38
|
+
# _(x).must_be_kind_of(T) → narrow x to T
|
|
39
|
+
# _(x).must_be_instance_of(T) → narrow x to T
|
|
40
|
+
# _(x).must_be_nil → narrow x to Constant<nil>
|
|
41
|
+
# _(x).must_equal(literal) → narrow x to Constant<literal>
|
|
42
|
+
# _(x).must_match(regex) → narrow x to String
|
|
43
|
+
#
|
|
44
|
+
# The legacy bare `x.must_be_kind_of(T)` form (Minitest <
|
|
45
|
+
# 6.0 monkey-patched onto Object) is intentionally NOT
|
|
46
|
+
# recognised — the receiver is the value itself rather
|
|
47
|
+
# than a wrapping `_(value)`, so the analyzer has nothing
|
|
48
|
+
# to narrow against. Users who still rely on the legacy
|
|
49
|
+
# form should migrate to `_(x).must_*`.
|
|
50
|
+
#
|
|
51
|
+
# ### (4) Minitest/spec `_(x).wont_*` (negative) ###
|
|
52
|
+
#
|
|
53
|
+
# _(x).wont_be_kind_of(T) → narrow x AWAY from T
|
|
54
|
+
# _(x).wont_be_nil → narrow x AWAY from nil
|
|
55
|
+
# _(x).wont_equal(literal) → narrow x AWAY from Constant<literal>
|
|
56
|
+
#
|
|
57
|
+
# ## Not yet recognised
|
|
58
|
+
#
|
|
59
|
+
# `assert_predicate(x, :foo?)` (custom predicate) /
|
|
60
|
+
# `assert_respond_to(x, :method)` / `assert_includes` /
|
|
61
|
+
# `assert_operator` / `assert_throws` /
|
|
62
|
+
# `assert_raises(T) { ... }` etc. — each either needs a
|
|
63
|
+
# carrier Rigor doesn't model today (predicate-state,
|
|
64
|
+
# respond-to-set) or a multi-edge fact that the
|
|
65
|
+
# `post_return_facts` slot can't express. Queued for
|
|
66
|
+
# follow-up slices.
|
|
67
|
+
module AssertionAnalyzer
|
|
68
|
+
module_function
|
|
69
|
+
|
|
70
|
+
# @param call_node [Prism::CallNode]
|
|
71
|
+
# @param environment [Rigor::Environment, nil]
|
|
72
|
+
# @return [Rigor::FlowContribution, nil]
|
|
73
|
+
def contribution_for(call_node, environment:)
|
|
74
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
75
|
+
|
|
76
|
+
fact =
|
|
77
|
+
assert_form_fact(call_node, environment: environment) ||
|
|
78
|
+
spec_form_fact(call_node, environment: environment)
|
|
79
|
+
return nil if fact.nil?
|
|
80
|
+
|
|
81
|
+
Rigor::FlowContribution.new(post_return_facts: [fact])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# --- assert_* / refute_* / assert_not_* form ---
|
|
85
|
+
|
|
86
|
+
# Maps each recognised assertion name to a tuple
|
|
87
|
+
# `[shape, negative]`. `shape` is one of:
|
|
88
|
+
#
|
|
89
|
+
# - :class_then_local — `assert_kind_of(T, x)`, T at
|
|
90
|
+
# args[0], local at args[1].
|
|
91
|
+
# - :nil_local — `assert_nil(x)`, local at args[0].
|
|
92
|
+
# - :literal_then_local — `assert_equal(literal, x)`,
|
|
93
|
+
# literal at args[0], local at args[1].
|
|
94
|
+
# - :regex_then_local — `assert_match(regex, x)`,
|
|
95
|
+
# regex at args[0], local at args[1].
|
|
96
|
+
ASSERT_FORM = {
|
|
97
|
+
assert_kind_of: [:class_then_local, false],
|
|
98
|
+
assert_instance_of: [:class_then_local, false],
|
|
99
|
+
refute_kind_of: [:class_then_local, true],
|
|
100
|
+
refute_instance_of: [:class_then_local, true],
|
|
101
|
+
assert_not_kind_of: [:class_then_local, true],
|
|
102
|
+
assert_not_instance_of: [:class_then_local, true],
|
|
103
|
+
assert_nil: [:nil_local, false],
|
|
104
|
+
refute_nil: [:nil_local, true],
|
|
105
|
+
assert_not_nil: [:nil_local, true],
|
|
106
|
+
assert_equal: [:literal_then_local, false],
|
|
107
|
+
refute_equal: [:literal_then_local, true],
|
|
108
|
+
assert_not_equal: [:literal_then_local, true],
|
|
109
|
+
assert_match: [:regex_then_local, false]
|
|
110
|
+
}.freeze
|
|
111
|
+
private_constant :ASSERT_FORM
|
|
112
|
+
|
|
113
|
+
def assert_form_fact(call_node, environment:)
|
|
114
|
+
return nil unless call_node.receiver.nil?
|
|
115
|
+
|
|
116
|
+
shape_negative = ASSERT_FORM[call_node.name]
|
|
117
|
+
return nil if shape_negative.nil?
|
|
118
|
+
|
|
119
|
+
shape, negative = shape_negative
|
|
120
|
+
args = call_node.arguments&.arguments || []
|
|
121
|
+
fact_for_shape(shape, args, negative: negative, environment: environment)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# --- _(x).must_* / .wont_* form ---
|
|
125
|
+
|
|
126
|
+
# Maps the spec-style matcher names to `[shape,
|
|
127
|
+
# negative]`. `shape`:
|
|
128
|
+
# - :class_arg — `_(x).must_be_kind_of(T)`, T at args[0].
|
|
129
|
+
# - :no_arg_nil — `_(x).must_be_nil`, no args.
|
|
130
|
+
# - :literal_arg — `_(x).must_equal(literal)`, literal at args[0].
|
|
131
|
+
# - :regex_arg — `_(x).must_match(regex)`, regex at args[0].
|
|
132
|
+
SPEC_MATCHER_FORM = {
|
|
133
|
+
must_be_kind_of: [:class_arg, false],
|
|
134
|
+
must_be_instance_of: [:class_arg, false],
|
|
135
|
+
must_be_a: [:class_arg, false],
|
|
136
|
+
must_be_an_instance_of: [:class_arg, false],
|
|
137
|
+
wont_be_kind_of: [:class_arg, true],
|
|
138
|
+
wont_be_instance_of: [:class_arg, true],
|
|
139
|
+
must_be_nil: [:no_arg_nil, false],
|
|
140
|
+
wont_be_nil: [:no_arg_nil, true],
|
|
141
|
+
must_equal: [:literal_arg, false],
|
|
142
|
+
wont_equal: [:literal_arg, true],
|
|
143
|
+
must_match: [:regex_arg, false]
|
|
144
|
+
}.freeze
|
|
145
|
+
private_constant :SPEC_MATCHER_FORM
|
|
146
|
+
|
|
147
|
+
def spec_form_fact(call_node, environment:)
|
|
148
|
+
shape_negative = SPEC_MATCHER_FORM[call_node.name]
|
|
149
|
+
return nil if shape_negative.nil?
|
|
150
|
+
|
|
151
|
+
target_local = spec_receiver_local(call_node)
|
|
152
|
+
return nil if target_local.nil?
|
|
153
|
+
|
|
154
|
+
shape, negative = shape_negative
|
|
155
|
+
args = call_node.arguments&.arguments || []
|
|
156
|
+
fact_for_spec_shape(shape, target_local, args, negative: negative, environment: environment)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `_(x)` returns a `Minitest::Expectation` wrapping x.
|
|
160
|
+
# Some specs use `value(x)` or `expect(x)` interchangeably
|
|
161
|
+
# (Minitest provides all three as aliases). Recognises the
|
|
162
|
+
# local-variable arg in any of those receiver-call shapes.
|
|
163
|
+
SPEC_WRAPPER_NAMES = %i[_ value expect].freeze
|
|
164
|
+
private_constant :SPEC_WRAPPER_NAMES
|
|
165
|
+
|
|
166
|
+
def spec_receiver_local(matcher_call)
|
|
167
|
+
recv = matcher_call.receiver
|
|
168
|
+
return nil unless recv.is_a?(Prism::CallNode)
|
|
169
|
+
return nil unless recv.receiver.nil? && SPEC_WRAPPER_NAMES.include?(recv.name)
|
|
170
|
+
|
|
171
|
+
args = recv.arguments&.arguments || []
|
|
172
|
+
return nil unless args.size == 1
|
|
173
|
+
return nil unless args.first.is_a?(Prism::LocalVariableReadNode)
|
|
174
|
+
|
|
175
|
+
args.first.name
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# --- shape resolvers ---
|
|
179
|
+
|
|
180
|
+
def fact_for_shape(shape, args, negative:, environment:)
|
|
181
|
+
case shape
|
|
182
|
+
when :class_then_local
|
|
183
|
+
return nil unless args.size == 2
|
|
184
|
+
return nil unless args[1].is_a?(Prism::LocalVariableReadNode)
|
|
185
|
+
|
|
186
|
+
type = nominal_type_for(args[0], environment: environment)
|
|
187
|
+
fact_for(args[1].name, type, negative: negative)
|
|
188
|
+
when :nil_local
|
|
189
|
+
return nil unless args.size == 1
|
|
190
|
+
return nil unless args[0].is_a?(Prism::LocalVariableReadNode)
|
|
191
|
+
|
|
192
|
+
fact_for(args[0].name, Rigor::Type::Combinator.constant_of(nil), negative: negative)
|
|
193
|
+
when :literal_then_local
|
|
194
|
+
return nil unless args.size == 2
|
|
195
|
+
return nil unless args[1].is_a?(Prism::LocalVariableReadNode)
|
|
196
|
+
|
|
197
|
+
literal = literal_value_for(args[0])
|
|
198
|
+
return nil if literal.equal?(NO_LITERAL)
|
|
199
|
+
|
|
200
|
+
fact_for(args[1].name, Rigor::Type::Combinator.constant_of(literal), negative: negative)
|
|
201
|
+
when :regex_then_local
|
|
202
|
+
return nil unless args.size == 2
|
|
203
|
+
return nil unless args[1].is_a?(Prism::LocalVariableReadNode)
|
|
204
|
+
return nil unless regex_literal?(args[0])
|
|
205
|
+
|
|
206
|
+
fact_for(args[1].name, Rigor::Type::Combinator.nominal_of("String"), negative: negative)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def fact_for_spec_shape(shape, target_local, args, negative:, environment:)
|
|
211
|
+
case shape
|
|
212
|
+
when :class_arg
|
|
213
|
+
return nil unless args.size == 1
|
|
214
|
+
|
|
215
|
+
type = nominal_type_for(args[0], environment: environment)
|
|
216
|
+
fact_for(target_local, type, negative: negative)
|
|
217
|
+
when :no_arg_nil
|
|
218
|
+
return nil unless args.empty?
|
|
219
|
+
|
|
220
|
+
fact_for(target_local, Rigor::Type::Combinator.constant_of(nil), negative: negative)
|
|
221
|
+
when :literal_arg
|
|
222
|
+
return nil unless args.size == 1
|
|
223
|
+
|
|
224
|
+
literal = literal_value_for(args[0])
|
|
225
|
+
return nil if literal.equal?(NO_LITERAL)
|
|
226
|
+
|
|
227
|
+
fact_for(target_local, Rigor::Type::Combinator.constant_of(literal), negative: negative)
|
|
228
|
+
when :regex_arg
|
|
229
|
+
return nil unless args.size == 1
|
|
230
|
+
return nil unless regex_literal?(args[0])
|
|
231
|
+
|
|
232
|
+
fact_for(target_local, Rigor::Type::Combinator.nominal_of("String"), negative: negative)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def fact_for(local_name, type, negative:)
|
|
237
|
+
return nil if type.nil?
|
|
238
|
+
|
|
239
|
+
Rigor::FlowContribution::Fact.new(
|
|
240
|
+
target_kind: :local,
|
|
241
|
+
target_name: local_name,
|
|
242
|
+
type: type,
|
|
243
|
+
negative: negative
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# --- helpers shared with rigor-rspec MatcherAnalyzer ---
|
|
248
|
+
|
|
249
|
+
def nominal_type_for(node, environment:)
|
|
250
|
+
class_name = constant_path_name(node)
|
|
251
|
+
return nil if class_name.nil?
|
|
252
|
+
|
|
253
|
+
if environment
|
|
254
|
+
environment.nominal_for_name(class_name) ||
|
|
255
|
+
Rigor::Type::Combinator.nominal_of(class_name)
|
|
256
|
+
else
|
|
257
|
+
Rigor::Type::Combinator.nominal_of(class_name)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
NO_LITERAL = Object.new.freeze
|
|
262
|
+
private_constant :NO_LITERAL
|
|
263
|
+
|
|
264
|
+
def literal_value_for(node)
|
|
265
|
+
case node
|
|
266
|
+
when Prism::IntegerNode then node.value
|
|
267
|
+
when Prism::FloatNode then node.value
|
|
268
|
+
when Prism::TrueNode then true
|
|
269
|
+
when Prism::FalseNode then false
|
|
270
|
+
when Prism::NilNode then nil
|
|
271
|
+
when Prism::StringNode then node.unescaped
|
|
272
|
+
when Prism::SymbolNode then node.unescaped.to_sym
|
|
273
|
+
else NO_LITERAL
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def regex_literal?(node)
|
|
278
|
+
node.is_a?(Prism::RegularExpressionNode) ||
|
|
279
|
+
node.is_a?(Prism::InterpolatedRegularExpressionNode)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def constant_path_name(node)
|
|
283
|
+
case node
|
|
284
|
+
when Prism::ConstantReadNode
|
|
285
|
+
node.name.to_s
|
|
286
|
+
when Prism::ConstantPathNode
|
|
287
|
+
parts = []
|
|
288
|
+
current = node
|
|
289
|
+
while current.is_a?(Prism::ConstantPathNode)
|
|
290
|
+
parts.unshift(current.name.to_s)
|
|
291
|
+
current = current.parent
|
|
292
|
+
end
|
|
293
|
+
case current
|
|
294
|
+
when nil then "::#{parts.join('::')}"
|
|
295
|
+
when Prism::ConstantReadNode then "#{current.name}::#{parts.join('::')}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/plugin"
|
|
4
|
+
|
|
5
|
+
require_relative "minitest/assertion_analyzer"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
# rigor-minitest — narrows locals through Minitest /
|
|
10
|
+
# Test::Unit assertions.
|
|
11
|
+
#
|
|
12
|
+
# Recognises every supported `assert_*` / `refute_*` /
|
|
13
|
+
# `assert_not_*` shape (Minitest core + Test::Unit aliases),
|
|
14
|
+
# plus the Minitest/spec `_(x).must_*` / `.wont_*` matchers
|
|
15
|
+
# (`expect(x)` / `value(x)` wrappers covered too). For each
|
|
16
|
+
# recognised assertion the plugin emits a Rigor
|
|
17
|
+
# `post_return_fact` whose `:local`-kind target narrows the
|
|
18
|
+
# named local on the post-call edge so downstream calls in
|
|
19
|
+
# the same `it` / `test_` / `def test_*` body resolve
|
|
20
|
+
# against the narrowed type.
|
|
21
|
+
#
|
|
22
|
+
# ## Why a single plugin covers two frameworks
|
|
23
|
+
#
|
|
24
|
+
# Minitest and Test::Unit share the `assert_*` / `refute_*`
|
|
25
|
+
# surface (Test::Unit's `assert_not_*` aliases are
|
|
26
|
+
# recognised by the same rule table). The matchers_vaccine
|
|
27
|
+
# gem layers RSpec-style matcher composition on top of
|
|
28
|
+
# Minitest's `must` API and is covered by the spec-style
|
|
29
|
+
# recogniser without additional wiring.
|
|
30
|
+
#
|
|
31
|
+
# ## Configuration
|
|
32
|
+
#
|
|
33
|
+
# No knobs in v0.1.0. Activate via `plugins: ["rigor-minitest"]`
|
|
34
|
+
# in `.rigor.yml`.
|
|
35
|
+
#
|
|
36
|
+
# ## Limitations (v0.1.0)
|
|
37
|
+
#
|
|
38
|
+
# - **No `assert_raises(T) { ... }`** — that's a block-shape
|
|
39
|
+
# matcher and Rigor's narrowing model is for
|
|
40
|
+
# straight-line locals.
|
|
41
|
+
# - **No `assert_predicate(x, :foo?)`** — needs a
|
|
42
|
+
# predicate-state carrier Rigor doesn't model today.
|
|
43
|
+
# - **No `assert_respond_to(x, :m)`** — same shape gap
|
|
44
|
+
# (respond-to-set carrier).
|
|
45
|
+
# - **No legacy bare `x.must_be_kind_of(T)`** — the
|
|
46
|
+
# receiver IS the value, so the analyzer has nothing to
|
|
47
|
+
# narrow against. Users should migrate to `_(x).must_*`.
|
|
48
|
+
# - **No `assert_in_delta` / `assert_operator`** — float-
|
|
49
|
+
# range / generic operator narrowing is future work.
|
|
50
|
+
# - **No `assert_throws(:tag) { ... }` / `assert_raises`** —
|
|
51
|
+
# block-form expectations need a separate slice.
|
|
52
|
+
class Minitest < Rigor::Plugin::Base
|
|
53
|
+
manifest(
|
|
54
|
+
id: "minitest",
|
|
55
|
+
version: "0.1.0",
|
|
56
|
+
description: "Narrows locals through Minitest / Test::Unit `assert_*` / `refute_*` " \
|
|
57
|
+
"and Minitest/spec `_(x).must_*` / `.wont_*` matchers."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Pillar 2 Slice 1 (rigor-minitest sibling) — emits
|
|
61
|
+
# `post_return_facts` for every recognised assertion. The
|
|
62
|
+
# engine routes `:local`-kind facts through
|
|
63
|
+
# `StatementEvaluator#apply_local_post_return_fact` (added
|
|
64
|
+
# in v0.1.8 for the rigor-rspec slice).
|
|
65
|
+
def flow_contribution_for(call_node:, scope:)
|
|
66
|
+
AssertionAnalyzer.contribution_for(call_node, environment: scope&.environment)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Rigor::Plugin.register(Minitest)
|
|
71
|
+
end
|
|
72
|
+
end
|