rigortype 0.2.7 → 0.2.8
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/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/15-type-protection-coverage.md +6 -4
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +96 -151
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +19 -26
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +43 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +27 -17
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +176 -291
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +774 -1152
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +509 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -817
- data/lib/rigor/inference/statement_evaluator.rb +706 -1111
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/scope.rbs +6 -0
- metadata +2 -1
|
@@ -8,59 +8,42 @@ require "rigor/flow_contribution/fact"
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Plugin
|
|
10
10
|
class Rspec < Rigor::Plugin::Base
|
|
11
|
-
# Recognises `expect(x).to MATCHER`
|
|
12
|
-
#
|
|
13
|
-
# `post_return_facts` that narrow the named local on the
|
|
14
|
-
# post-call edge.
|
|
11
|
+
# Recognises `expect(x).to MATCHER` patterns at per-call recognition time and emits
|
|
12
|
+
# `post_return_facts` that narrow the named local on the post-call edge.
|
|
15
13
|
#
|
|
16
|
-
# The supported matchers are the lowest-false-positive
|
|
17
|
-
# floor of the RSpec matcher DSL:
|
|
14
|
+
# The supported matchers are the lowest-false-positive floor of the RSpec matcher DSL:
|
|
18
15
|
#
|
|
19
|
-
# - `be_a(T)` / `be_kind_of(T)` — `x is_a?(T)` style class
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# uniformly, so the distinction with `be_a` is observed
|
|
24
|
-
# at the carrier level but not the runtime).
|
|
16
|
+
# - `be_a(T)` / `be_kind_of(T)` — `x is_a?(T)` style class membership; narrows `x` to `T`.
|
|
17
|
+
# - `be_instance_of(T)` — exact-class match; narrows `x` to `T` (the engine currently treats
|
|
18
|
+
# Nominal[T] uniformly, so the distinction with `be_a` is observed at the carrier level but not the
|
|
19
|
+
# runtime).
|
|
25
20
|
# - `be_nil` — narrows `x` to `Constant<nil>`.
|
|
26
|
-
# - `eq(LITERAL)` for a literal-value argument — narrows
|
|
27
|
-
# `x` to `Constant<literal>`.
|
|
21
|
+
# - `eq(LITERAL)` for a literal-value argument — narrows `x` to `Constant<literal>`.
|
|
28
22
|
#
|
|
29
|
-
# The matchers below this floor (`be_truthy` / `be_falsey`
|
|
30
|
-
# / `
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# `falsey_facts` rather than `post_return_facts`) or
|
|
34
|
-
# diagnostic-only enforcement; both are queued for follow-up
|
|
35
|
-
# slices.
|
|
23
|
+
# The matchers below this floor (`be_truthy` / `be_falsey` / `be_within` / `be > / < / >=` /
|
|
24
|
+
# `include` / `start_with` / `match(regex)` / `raise_error` / receive-style mocks) require either
|
|
25
|
+
# edge-aware fragments (`truthy_facts` / `falsey_facts` rather than `post_return_facts`) or
|
|
26
|
+
# diagnostic-only enforcement; both are queued for follow-up slices.
|
|
36
27
|
#
|
|
37
28
|
# The analyzer fires ONLY when:
|
|
38
29
|
#
|
|
39
|
-
# 1. The call node is one of `<recv>.to(matcher)` /
|
|
40
|
-
# `<recv>.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# `expect { ... }.to raise_error`) fall through.
|
|
48
|
-
# 3. The matcher is one of the recognised forms above
|
|
49
|
-
# (`be_a` / `be_kind_of` / `be_instance_of` /
|
|
50
|
-
# `be_an_instance_of` / `be_nil` / `eq(literal)` /
|
|
51
|
-
# `eql(literal)` / `match(/regex/)`).
|
|
30
|
+
# 1. The call node is one of `<recv>.to(matcher)` / `<recv>.not_to(matcher)` /
|
|
31
|
+
# `<recv>.to_not(matcher)`. `not_to` / `to_not` flip the fact's `negative` flag so the engine
|
|
32
|
+
# narrows AWAY from the matcher's type (e.g. `not_to be_nil` removes nil from the receiver).
|
|
33
|
+
# 2. The receiver is `expect(<local_var>)` — exactly one positional argument that's a
|
|
34
|
+
# LocalVariableReadNode. Composite receivers (`expect(foo.bar)`, `expect { ... }.to raise_error`)
|
|
35
|
+
# fall through.
|
|
36
|
+
# 3. The matcher is one of the recognised forms above (`be_a` / `be_kind_of` / `be_instance_of` /
|
|
37
|
+
# `be_an_instance_of` / `be_nil` / `eq(literal)` / `eql(literal)` / `match(/regex/)`).
|
|
52
38
|
module MatcherAnalyzer
|
|
53
39
|
module_function
|
|
54
40
|
|
|
55
|
-
# @param call_node [Prism::CallNode] the call whose
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# class-name resolution falls back to a bare
|
|
62
|
-
# `Nominal[<name>]` carrier (sound — the receiver
|
|
63
|
-
# constant may be a user class not in RBS).
|
|
41
|
+
# @param call_node [Prism::CallNode] the call whose contribution we're computing. Returns nil when
|
|
42
|
+
# the call shape does not match `expect(local).to matcher`.
|
|
43
|
+
# @param environment [Rigor::Environment, nil] the surrounding environment used to resolve a
|
|
44
|
+
# matcher's class-name argument to a `Type::Nominal`. When nil, class-name resolution falls back
|
|
45
|
+
# to a bare `Nominal[<name>]` carrier (sound — the receiver constant may be a user class not in
|
|
46
|
+
# RBS).
|
|
64
47
|
# @return [Rigor::FlowContribution, nil]
|
|
65
48
|
def contribution_for(call_node, environment:)
|
|
66
49
|
verb = assertion_verb(call_node)
|
|
@@ -136,11 +119,9 @@ module Rigor
|
|
|
136
119
|
when :eq, :eql
|
|
137
120
|
constant_type_for_literal_arg(matcher)
|
|
138
121
|
when :match
|
|
139
|
-
# `match(/regex/)` narrows x to String. `match("...")`
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# String, but we can only assert that for a literal
|
|
143
|
-
# regex.
|
|
122
|
+
# `match(/regex/)` narrows x to String. `match("...")` or `match(arbitrary_object)` falls
|
|
123
|
+
# through — the broader matcher dispatch needs the receiver to be a String, but we can only
|
|
124
|
+
# assert that for a literal regex.
|
|
144
125
|
string_type_for_regex_arg(matcher)
|
|
145
126
|
end
|
|
146
127
|
end
|
|
@@ -184,10 +165,9 @@ module Rigor
|
|
|
184
165
|
NO_LITERAL = Object.new.freeze
|
|
185
166
|
private_constant :NO_LITERAL
|
|
186
167
|
|
|
187
|
-
# Returns the Ruby value of a literal-AST argument, or
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
# case: integer, float, true/false/nil, string, symbol.
|
|
168
|
+
# Returns the Ruby value of a literal-AST argument, or `NO_LITERAL` when the node isn't a
|
|
169
|
+
# recognised literal shape. Recognised forms cover the common `eq(literal)` case: integer, float,
|
|
170
|
+
# true/false/nil, string, symbol.
|
|
191
171
|
def literal_value_for(node)
|
|
192
172
|
case node
|
|
193
173
|
when Prism::IntegerNode then node.value
|
|
@@ -206,10 +186,8 @@ module Rigor
|
|
|
206
186
|
args.nil? || args.empty?
|
|
207
187
|
end
|
|
208
188
|
|
|
209
|
-
# Renders a `Prism::ConstantReadNode` /
|
|
210
|
-
#
|
|
211
|
-
# String. Returns nil when the node isn't a constant
|
|
212
|
-
# reference.
|
|
189
|
+
# Renders a `Prism::ConstantReadNode` / `Prism::ConstantPathNode` chain as a `"Foo::Bar"` String.
|
|
190
|
+
# Returns nil when the node isn't a constant reference.
|
|
213
191
|
def constant_path_name(node)
|
|
214
192
|
case node
|
|
215
193
|
when Prism::ConstantReadNode
|
|
@@ -6,52 +6,40 @@ require "rigor/source/literals"
|
|
|
6
6
|
module Rigor
|
|
7
7
|
module Plugin
|
|
8
8
|
class Rspec < Rigor::Plugin::Base
|
|
9
|
-
# Walks an RSpec spec file's AST and yields, for each
|
|
10
|
-
# describe
|
|
11
|
-
# `RSpec.describe`), a `Scope` value with the `let`
|
|
12
|
-
# and `subject` declarations recorded inside that
|
|
9
|
+
# Walks an RSpec spec file's AST and yields, for each describe / context block (including the outer
|
|
10
|
+
# `RSpec.describe`), a `Scope` value with the `let` and `subject` declarations recorded inside that
|
|
13
11
|
# scope.
|
|
14
12
|
#
|
|
15
|
-
# Scope hierarchy is preserved: each `Scope` carries
|
|
16
|
-
#
|
|
17
|
-
# hierarchy to detect cross-scope shadowing.
|
|
13
|
+
# Scope hierarchy is preserved: each `Scope` carries a list of its `nested_scopes`. The analyzer uses
|
|
14
|
+
# the hierarchy to detect cross-scope shadowing.
|
|
18
15
|
#
|
|
19
|
-
# Recognised scope methods (when called without a
|
|
20
|
-
# receiver, or with `RSpec` as the receiver):
|
|
16
|
+
# Recognised scope methods (when called without a receiver, or with `RSpec` as the receiver):
|
|
21
17
|
#
|
|
22
|
-
# - `describe` / `context` — both open a new nested
|
|
23
|
-
# scope.
|
|
18
|
+
# - `describe` / `context` — both open a new nested scope.
|
|
24
19
|
# - `RSpec.describe` — the outermost scope.
|
|
25
20
|
#
|
|
26
|
-
# Recognised declaration methods (inside any scope,
|
|
27
|
-
# called without a receiver):
|
|
21
|
+
# Recognised declaration methods (inside any scope, called without a receiver):
|
|
28
22
|
#
|
|
29
|
-
# - `let(:name) { ... }` — caches the block result
|
|
30
|
-
#
|
|
31
|
-
# - `let!(:name) { ... }` — same, but evaluated
|
|
32
|
-
# eagerly via a `before` hook; same shape for our
|
|
23
|
+
# - `let(:name) { ... }` — caches the block result per-example; recorded as a Declaration.
|
|
24
|
+
# - `let!(:name) { ... }` — same, but evaluated eagerly via a `before` hook; same shape for our
|
|
33
25
|
# purposes.
|
|
34
|
-
# - `subject(:name) { ... }` — special-cases name
|
|
35
|
-
# `:subject` when called without a name.
|
|
26
|
+
# - `subject(:name) { ... }` — special-cases name `:subject` when called without a name.
|
|
36
27
|
module ScopeWalker
|
|
37
28
|
SCOPE_METHODS = %i[describe context].freeze
|
|
38
29
|
DECLARATION_METHODS = %i[let let! subject].freeze
|
|
39
30
|
|
|
40
31
|
# @!attribute [r] kind
|
|
41
|
-
# `:describe`, `:context`, or `:rspec_describe` for
|
|
42
|
-
# the root.
|
|
32
|
+
# `:describe`, `:context`, or `:rspec_describe` for the root.
|
|
43
33
|
# @!attribute [r] declarations
|
|
44
34
|
# `Array<Declaration>` declared in this scope.
|
|
45
35
|
# @!attribute [r] nested_scopes
|
|
46
36
|
# `Array<Scope>` nested under this scope.
|
|
47
37
|
# @!attribute [r] location
|
|
48
|
-
# `Prism::Location` of the call node that opened
|
|
49
|
-
# this scope.
|
|
38
|
+
# `Prism::Location` of the call node that opened this scope.
|
|
50
39
|
Scope = Struct.new(:kind, :declarations, :nested_scopes, :location, keyword_init: true)
|
|
51
40
|
|
|
52
41
|
# @!attribute [r] name
|
|
53
|
-
# `Symbol` declared name (`:user`, `:subject`,
|
|
54
|
-
# ...).
|
|
42
|
+
# `Symbol` declared name (`:user`, `:subject`, ...).
|
|
55
43
|
# @!attribute [r] kind
|
|
56
44
|
# `:let`, `:let!`, or `:subject`.
|
|
57
45
|
# @!attribute [r] location
|
|
@@ -62,18 +50,15 @@ module Rigor
|
|
|
62
50
|
|
|
63
51
|
module_function
|
|
64
52
|
|
|
65
|
-
# Walks the parsed file and returns an array of
|
|
66
|
-
#
|
|
67
|
-
# separate root). Files with no recognised scopes
|
|
68
|
-
# return an empty array.
|
|
53
|
+
# Walks the parsed file and returns an array of top-level scopes (each `RSpec.describe` is a
|
|
54
|
+
# separate root). Files with no recognised scopes return an empty array.
|
|
69
55
|
def collect_scopes(root)
|
|
70
56
|
scopes = []
|
|
71
57
|
walk_top_level(root, scopes)
|
|
72
58
|
scopes
|
|
73
59
|
end
|
|
74
60
|
|
|
75
|
-
# Walks every scope in a tree (root + descendants)
|
|
76
|
-
# and yields each in turn.
|
|
61
|
+
# Walks every scope in a tree (root + descendants) and yields each in turn.
|
|
77
62
|
def each_scope(scope, &)
|
|
78
63
|
yield scope
|
|
79
64
|
scope.nested_scopes.each { |child| each_scope(child, &) }
|
|
@@ -89,8 +74,7 @@ module Rigor
|
|
|
89
74
|
end
|
|
90
75
|
end
|
|
91
76
|
|
|
92
|
-
# Returns true for `RSpec.describe ... do |...| ...
|
|
93
|
-
# end` calls.
|
|
77
|
+
# Returns true for `RSpec.describe ... do |...| ... end` calls.
|
|
94
78
|
def rspec_describe_call?(node)
|
|
95
79
|
return false unless node.is_a?(Prism::CallNode)
|
|
96
80
|
return false unless node.name == :describe
|
|
@@ -100,10 +84,8 @@ module Rigor
|
|
|
100
84
|
%w[RSpec ::RSpec].include?(receiver_name)
|
|
101
85
|
end
|
|
102
86
|
|
|
103
|
-
# Returns true for `describe ... do ... end` /
|
|
104
|
-
#
|
|
105
|
-
# explicit receiver — `RSpec.describe` is handled
|
|
106
|
-
# separately by `rspec_describe_call?`).
|
|
87
|
+
# Returns true for `describe ... do ... end` / `context ... do ... end` (called without an
|
|
88
|
+
# explicit receiver — `RSpec.describe` is handled separately by `rspec_describe_call?`).
|
|
107
89
|
def nested_scope_call?(node)
|
|
108
90
|
node.is_a?(Prism::CallNode) &&
|
|
109
91
|
SCOPE_METHODS.include?(node.name) &&
|
|
@@ -117,9 +99,8 @@ module Rigor
|
|
|
117
99
|
node.receiver.nil?
|
|
118
100
|
end
|
|
119
101
|
|
|
120
|
-
# Constructs a Scope from a describe / context /
|
|
121
|
-
#
|
|
122
|
-
# for declarations + nested scopes.
|
|
102
|
+
# Constructs a Scope from a describe / context / RSpec.describe call node. Walks the block body for
|
|
103
|
+
# declarations + nested scopes.
|
|
123
104
|
def build_scope(call_node, kind:)
|
|
124
105
|
declarations = []
|
|
125
106
|
nested = []
|
|
@@ -146,8 +127,7 @@ module Rigor
|
|
|
146
127
|
|
|
147
128
|
def build_declaration(call_node)
|
|
148
129
|
first_arg = call_node.arguments&.arguments&.first
|
|
149
|
-
# `subject(&block)` (no name) defaults to the
|
|
150
|
-
# implicit subject; record it as `:subject`.
|
|
130
|
+
# `subject(&block)` (no name) defaults to the implicit subject; record it as `:subject`.
|
|
151
131
|
if first_arg.nil?
|
|
152
132
|
return nil unless call_node.name == :subject
|
|
153
133
|
|
|
@@ -10,62 +10,41 @@ require_relative "rspec/let_type_resolver"
|
|
|
10
10
|
|
|
11
11
|
module Rigor
|
|
12
12
|
module Plugin
|
|
13
|
-
# rigor-rspec — validates RSpec `let` / `subject`
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# and binds `let`/`subject` locals to their inferred
|
|
17
|
-
# return types inside `it` bodies.
|
|
13
|
+
# rigor-rspec — validates RSpec `let` / `subject` declarations within each describe / context scope,
|
|
14
|
+
# narrows `expect(x).to MATCHER` assertions downstream, and binds `let`/`subject` locals to their
|
|
15
|
+
# inferred return types inside `it` bodies.
|
|
18
16
|
#
|
|
19
17
|
# Tier 3A of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
|
|
20
18
|
# Ships four checks:
|
|
21
19
|
#
|
|
22
|
-
# 1. **Duplicate `let` / `subject` declarations** in
|
|
23
|
-
# the
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# bug.
|
|
27
|
-
# 2. **Self-referencing `let` / `subject`** — calling
|
|
28
|
-
# the declared name *inside* its own block body
|
|
29
|
-
# (`error`). At runtime this infinite-loops; users
|
|
30
|
-
# typically meant to call a different method or
|
|
20
|
+
# 1. **Duplicate `let` / `subject` declarations** in the same scope (`warning`). RSpec's runtime lets
|
|
21
|
+
# the last declaration win, so the first one is silently shadowed — almost always a copy-paste bug.
|
|
22
|
+
# 2. **Self-referencing `let` / `subject`** — calling the declared name *inside* its own block body
|
|
23
|
+
# (`error`). At runtime this infinite-loops; users typically meant to call a different method or
|
|
31
24
|
# forgot to introduce a `super`.
|
|
32
|
-
# 3. **`expect(x).to MATCHER` narrowing** — narrows
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# 4. **`let`/`subject` local binding** — binds `let`
|
|
36
|
-
# / `subject` names in `it` bodies to the block's
|
|
25
|
+
# 3. **`expect(x).to MATCHER` narrowing** — narrows the named local `x` on the post-call edge for eight
|
|
26
|
+
# matchers (see `MatcherAnalyzer`).
|
|
27
|
+
# 4. **`let`/`subject` local binding** — binds `let` / `subject` names in `it` bodies to the block's
|
|
37
28
|
# inferred return type (see `LetTypeResolver`).
|
|
38
29
|
#
|
|
39
30
|
# ## Configuration
|
|
40
31
|
#
|
|
41
|
-
# No configuration knobs. The plugin walks every
|
|
42
|
-
#
|
|
43
|
-
# spec files outside the project's `paths:` are not
|
|
44
|
-
# scanned.
|
|
32
|
+
# No configuration knobs. The plugin walks every analysed file for `RSpec.describe ... do` blocks; spec
|
|
33
|
+
# files outside the project's `paths:` are not scanned.
|
|
45
34
|
#
|
|
46
35
|
# ## Limitations
|
|
47
36
|
#
|
|
48
|
-
# - **No let-typo detection.** Detecting an `it`
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# - **No
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# the engine's
|
|
59
|
-
# diagnostics and needs careful coordination to avoid
|
|
60
|
-
# double-firing.
|
|
61
|
-
# - **No shared-context resolution.** `include_context`,
|
|
62
|
-
# `shared_context`, and `it_behaves_like` are
|
|
63
|
-
# recognised as scope-opening calls but their
|
|
64
|
-
# declarations are not pulled into the host scope.
|
|
65
|
-
# - **Constant validation is not done here.**
|
|
66
|
-
# `RSpec.describe SomeClass do` does not validate
|
|
67
|
-
# `SomeClass`; the engine's `inference.unresolved-constant`
|
|
68
|
-
# already catches that.
|
|
37
|
+
# - **No let-typo detection.** Detecting an `it` block's reference to a misspelled `let` name requires
|
|
38
|
+
# resolving every method call inside the block against the let scope chain, the included modules,
|
|
39
|
+
# the matchers DSL, and helper methods. Reliable diagnostics here need a much heavier walker — see
|
|
40
|
+
# the README's `Future direction`.
|
|
41
|
+
# - **No mock-target validation.** `expect(x).to receive(:nme)` validating against `x`'s methods is a
|
|
42
|
+
# separate slice; it overlaps with the engine's general method-existence diagnostics and needs
|
|
43
|
+
# careful coordination to avoid double-firing.
|
|
44
|
+
# - **No shared-context resolution.** `include_context`, `shared_context`, and `it_behaves_like` are
|
|
45
|
+
# recognised as scope-opening calls but their declarations are not pulled into the host scope.
|
|
46
|
+
# - **Constant validation is not done here.** `RSpec.describe SomeClass do` does not validate
|
|
47
|
+
# `SomeClass`; the engine's `inference.unresolved-constant` already catches that.
|
|
69
48
|
class Rspec < Rigor::Plugin::Base
|
|
70
49
|
manifest(
|
|
71
50
|
id: "rspec",
|
|
@@ -79,10 +58,9 @@ module Rigor
|
|
|
79
58
|
{ plugin_id: "factorybot", name: :factory_index, optional: true }
|
|
80
59
|
],
|
|
81
60
|
additional_initializers: [
|
|
82
|
-
# ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`,
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# widening that would otherwise appear on those ivars in `it` / `specify`
|
|
61
|
+
# ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`, and `subject { @ivar = … }`
|
|
62
|
+
# establish ivar state before `it` bodies run. Declaring them here suppresses the
|
|
63
|
+
# read-before-write nil widening that would otherwise appear on those ivars in `it` / `specify`
|
|
86
64
|
# sibling blocks.
|
|
87
65
|
Rigor::Plugin::AdditionalInitializer.new(
|
|
88
66
|
receiver_constraint: "RSpec::ExampleGroup",
|
|
@@ -92,53 +70,44 @@ module Rigor
|
|
|
92
70
|
)
|
|
93
71
|
|
|
94
72
|
def init(_services)
|
|
95
|
-
# Per-path `LetScopeIndex` cache. The let-binding
|
|
96
|
-
#
|
|
97
|
-
# consult the index per call node; building it once
|
|
98
|
-
# per file is essential for performance.
|
|
73
|
+
# Per-path `LetScopeIndex` cache. The let-binding `dynamic_return` rule (and its `file_methods:`
|
|
74
|
+
# gate) consult the index per call node; building it once per file is essential for performance.
|
|
99
75
|
@let_index_cache = {}
|
|
100
76
|
end
|
|
101
77
|
|
|
102
78
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
103
|
-
# Build the let-scope index for this file while we
|
|
104
|
-
#
|
|
105
|
-
# picks it up from `@let_index_cache` keyed on path.
|
|
79
|
+
# Build the let-scope index for this file while we have the parsed root in hand — the let-binding
|
|
80
|
+
# rule picks it up from `@let_index_cache` keyed on path.
|
|
106
81
|
@let_index_cache[path] ||= LetScopeIndex.build(root)
|
|
107
82
|
Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
|
|
108
83
|
end
|
|
109
84
|
|
|
110
|
-
# ADR-37 slice 2 — matcher narrowing
|
|
111
|
-
# (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
|
|
85
|
+
# ADR-37 slice 2 — matcher narrowing (`expect(x).to be_a(T)` → `post_return_facts` on `x`),
|
|
112
86
|
# method-gated by the engine on the expectation verbs.
|
|
113
87
|
narrowing_facts methods: %i[to not_to to_not] do |call_node, scope|
|
|
114
88
|
MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
|
|
115
89
|
end
|
|
116
90
|
|
|
117
|
-
# ADR-52 slice 5a — binds local reads in `it` /
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# once per analysed file and consults the block only for a listed
|
|
123
|
-
# name; the line-scoped shadowing resolution stays in the block.
|
|
91
|
+
# ADR-52 slice 5a — binds local reads in `it` / spec bodies to their `let(:name) { ... }` block's
|
|
92
|
+
# inferred return type. The name set varies per file (each spec file's `describe`/`let` structure),
|
|
93
|
+
# so the rule gates on the per-file `file_methods:` form: the engine resolves the file's let names
|
|
94
|
+
# once per analysed file and consults the block only for a listed name; the line-scoped shadowing
|
|
95
|
+
# resolution stays in the block.
|
|
124
96
|
dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
|
|
125
97
|
let_binding_return_type(call_node, scope)
|
|
126
98
|
end
|
|
127
99
|
|
|
128
100
|
private
|
|
129
101
|
|
|
130
|
-
# The `file_methods:` gate set — every `let` / `subject` name the
|
|
131
|
-
#
|
|
132
|
-
# own `let_block_at` line-scoped lookup (a name read outside its
|
|
102
|
+
# The `file_methods:` gate set — every `let` / `subject` name the file declares anywhere. A safe
|
|
103
|
+
# over-approximation of the block's own `let_block_at` line-scoped lookup (a name read outside its
|
|
133
104
|
# describe scope passes the gate and is declined by the block).
|
|
134
105
|
def let_names_for(path)
|
|
135
106
|
let_scope_index_for(path)&.let_names || []
|
|
136
107
|
end
|
|
137
108
|
|
|
138
|
-
# When the call node is a no-receiver
|
|
139
|
-
#
|
|
140
|
-
# `describe` block whose lets include a matching name,
|
|
141
|
-
# return the let block's inferred type.
|
|
109
|
+
# When the call node is a no-receiver method call (`user`, `subject`, etc.) inside an RSpec
|
|
110
|
+
# `describe` block whose lets include a matching name, return the let block's inferred type.
|
|
142
111
|
def let_binding_return_type(call_node, scope)
|
|
143
112
|
return nil if scope.nil?
|
|
144
113
|
return nil unless candidate_call?(call_node)
|
|
@@ -163,10 +132,9 @@ module Rigor
|
|
|
163
132
|
call_node.is_a?(Prism::CallNode) &&
|
|
164
133
|
call_node.receiver.nil? &&
|
|
165
134
|
call_node.block.nil? &&
|
|
166
|
-
# Calls with arguments are matcher / DSL invocations,
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
# uses to expose let / subject in `it` bodies.
|
|
135
|
+
# Calls with arguments are matcher / DSL invocations, not let-bound name reads. `subject` /
|
|
136
|
+
# `user` etc. without args are the implicit-method-call shape RSpec uses to expose let / subject
|
|
137
|
+
# in `it` bodies.
|
|
170
138
|
(call_node.arguments.nil? || call_node.arguments.arguments.empty?)
|
|
171
139
|
end
|
|
172
140
|
|
|
@@ -7,48 +7,39 @@ require_relative "http_status_codes"
|
|
|
7
7
|
module Rigor
|
|
8
8
|
module Plugin
|
|
9
9
|
class RspecRails < Rigor::Plugin::Base
|
|
10
|
-
# Validates `expect(response).to have_http_status(arg)` /
|
|
11
|
-
# the
|
|
12
|
-
# bare matcher form `should have_http_status(arg)`.
|
|
10
|
+
# Validates `expect(response).to have_http_status(arg)` / the negated `.not_to have_http_status(arg)`
|
|
11
|
+
# and the bare matcher form `should have_http_status(arg)`.
|
|
13
12
|
#
|
|
14
13
|
# Recognised argument shapes:
|
|
15
14
|
#
|
|
16
|
-
# - **IntegerNode**: must be in `100..599`. Numbers outside
|
|
17
|
-
#
|
|
18
|
-
# - **SymbolNode**: must be one of the known
|
|
19
|
-
# `
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# - **
|
|
25
|
-
#
|
|
26
|
-
# second-guess the user's intent for the String form).
|
|
27
|
-
# - **Anything else** (variable, method call, computed
|
|
28
|
-
# expression): skipped — the plugin can't statically
|
|
29
|
-
# prove the runtime value.
|
|
15
|
+
# - **IntegerNode**: must be in `100..599`. Numbers outside that range fire
|
|
16
|
+
# `have_http_status.out-of-range`.
|
|
17
|
+
# - **SymbolNode**: must be one of the known `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys OR a Rails
|
|
18
|
+
# status-group alias (`:success` / `:successful` / `:missing` / `:redirect` / `:error` /
|
|
19
|
+
# `:client_error` / `:server_error` / `:informational`). Unknown symbols fire
|
|
20
|
+
# `have_http_status.unknown-symbol` (typo-flavoured).
|
|
21
|
+
# - **StringNode**: passed through silently (a literal `"200"` is accepted by Rails at runtime; we
|
|
22
|
+
# don't second-guess the user's intent for the String form).
|
|
23
|
+
# - **Anything else** (variable, method call, computed expression): skipped — the plugin can't
|
|
24
|
+
# statically prove the runtime value.
|
|
30
25
|
#
|
|
31
|
-
# Walks every `Prism::CallNode` looking for matcher
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# in shared-context bodies; the diagnostic is the same
|
|
36
|
-
# regardless of the surrounding chain.
|
|
26
|
+
# Walks every `Prism::CallNode` looking for matcher invocations rather than only the
|
|
27
|
+
# `expect(...).to ...` chain because `have_http_status` is also used as a plain matcher inside Rails'
|
|
28
|
+
# `assert_response` shim and in shared-context bodies; the diagnostic is the same regardless of the
|
|
29
|
+
# surrounding chain.
|
|
37
30
|
module HaveHttpStatusAnalyzer
|
|
38
|
-
# One matcher violation: the kebab-case `rule` and the
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# matcher's `message_loc` via `Plugin::Base#diagnostic`.
|
|
31
|
+
# One matcher violation: the kebab-case `rule` and the human-readable `message`. Carries no
|
|
32
|
+
# location/path — the caller (the `node_rule` block) positions the diagnostic at the matcher's
|
|
33
|
+
# `message_loc` via `Plugin::Base#diagnostic`.
|
|
42
34
|
Violation = Struct.new(:rule, :message, keyword_init: true)
|
|
43
35
|
|
|
44
36
|
MATCHER_NAME = :have_http_status
|
|
45
37
|
|
|
46
38
|
module_function
|
|
47
39
|
|
|
48
|
-
# The matcher violation for a single CallNode, or nil when the
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# walk, so this is per-node logic — no traversal here.
|
|
40
|
+
# The matcher violation for a single CallNode, or nil when the node is not a
|
|
41
|
+
# `have_http_status(<int|symbol>)` matcher or its argument is statically valid. ADR-37: the engine
|
|
42
|
+
# owns the walk, so this is per-node logic — no traversal here.
|
|
52
43
|
#
|
|
53
44
|
# @param call_node [Prism::Node]
|
|
54
45
|
# @return [Violation, nil]
|
|
@@ -58,12 +49,9 @@ module Rigor
|
|
|
58
49
|
diagnostic_for(call_node)
|
|
59
50
|
end
|
|
60
51
|
|
|
61
|
-
# `have_http_status` is a matcher — called either with
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
# Either way we want the call node whose name is
|
|
65
|
-
# `have_http_status` and that carries exactly one
|
|
66
|
-
# positional argument.
|
|
52
|
+
# `have_http_status` is a matcher — called either with no receiver (`have_http_status(200)` inside
|
|
53
|
+
# `.to(...)`) or with a receiver chain we don't care about here. Either way we want the call node
|
|
54
|
+
# whose name is `have_http_status` and that carries exactly one positional argument.
|
|
67
55
|
def call_to_matcher?(node)
|
|
68
56
|
node.is_a?(Prism::CallNode) &&
|
|
69
57
|
node.name == MATCHER_NAME &&
|
|
@@ -97,8 +85,8 @@ module Rigor
|
|
|
97
85
|
def symbol_violation(symbol_node)
|
|
98
86
|
sym = symbol_node.unescaped.to_sym
|
|
99
87
|
known = HttpStatusCodes.known_symbols
|
|
100
|
-
# ADR-39: Rack unavailable → the accepted symbol set is
|
|
101
|
-
#
|
|
88
|
+
# ADR-39: Rack unavailable → the accepted symbol set is unknowable, so decline rather than flag
|
|
89
|
+
# from a guess.
|
|
102
90
|
return nil if known.nil?
|
|
103
91
|
return nil if known.include?(sym)
|
|
104
92
|
|
|
@@ -6,18 +6,15 @@ module Rigor
|
|
|
6
6
|
# The set of HTTP-status symbols `have_http_status` accepts.
|
|
7
7
|
#
|
|
8
8
|
# ADR-39 — the HTTP status symbols come from the **real**
|
|
9
|
-
# `Rack::Utils::SYMBOL_TO_STATUS_CODE` (the authority `have_http_status`
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# set is unknowable, so {known_symbols} returns `nil` and the caller
|
|
15
|
-
# **declines** to flag anything — reduced coverage, never a guess.
|
|
9
|
+
# `Rack::Utils::SYMBOL_TO_STATUS_CODE` (the authority `have_http_status` itself resolves through
|
|
10
|
+
# `Rack::Utils.status_code`), not a vendored snapshot: a snapshot that lagged the installed Rack
|
|
11
|
+
# would flag a newly-added status symbol as a false `unknown-symbol` (a false positive on working
|
|
12
|
+
# code). When Rack cannot be loaded the symbol set is unknowable, so {known_symbols} returns `nil`
|
|
13
|
+
# and the caller **declines** to flag anything — reduced coverage, never a guess.
|
|
16
14
|
#
|
|
17
|
-
# The Rails status-group aliases (`:success`, `:missing`, …) are a
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# they are kept as a constant rather than pulling in actionpack.
|
|
15
|
+
# The Rails status-group aliases (`:success`, `:missing`, …) are a small, stable convention set
|
|
16
|
+
# defined by Rails (`ActionDispatch::TestResponse`) — not a growing catalogue — so they are kept as a
|
|
17
|
+
# constant rather than pulling in actionpack.
|
|
21
18
|
module HttpStatusCodes
|
|
22
19
|
RAILS_STATUS_GROUP_ALIASES = %i[
|
|
23
20
|
informational success successful redirect
|
|
@@ -28,11 +25,10 @@ module Rigor
|
|
|
28
25
|
|
|
29
26
|
module_function
|
|
30
27
|
|
|
31
|
-
# The authoritative set of accepted `have_http_status` symbols
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# symbol as unknown (ADR-39: never guess from a stale copy).
|
|
28
|
+
# The authoritative set of accepted `have_http_status` symbols (real Rack status symbols ∪ the
|
|
29
|
+
# Rails group aliases), or `nil` when it cannot be determined because Rack is unavailable. A `nil`
|
|
30
|
+
# return means "decline" — the caller MUST NOT flag any symbol as unknown (ADR-39: never guess from
|
|
31
|
+
# a stale copy).
|
|
36
32
|
def known_symbols
|
|
37
33
|
rack = rack_status_symbols
|
|
38
34
|
return nil if rack.nil?
|
|
@@ -40,9 +36,8 @@ module Rigor
|
|
|
40
36
|
rack | RAILS_STATUS_GROUP_ALIASES
|
|
41
37
|
end
|
|
42
38
|
|
|
43
|
-
# The real `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys, or `nil` when
|
|
44
|
-
#
|
|
45
|
-
# dependency; the plugin brings it); memoised for the process.
|
|
39
|
+
# The real `Rack::Utils::SYMBOL_TO_STATUS_CODE` keys, or `nil` when Rack is not loadable. Lazy
|
|
40
|
+
# require (core Rigor carries no Rack dependency; the plugin brings it); memoised for the process.
|
|
46
41
|
def rack_status_symbols
|
|
47
42
|
return @rack_status_symbols if defined?(@rack_status_symbols)
|
|
48
43
|
|