rigortype 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/08-understanding-errors.md +3 -3
- data/docs/manual/01-installation.md +1 -0
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +27 -4
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- 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 +248 -186
- 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 +62 -48
- 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 +46 -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_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +34 -18
- 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/missing_gem_constant_index.rb +128 -0
- 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 +225 -304
- 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 +794 -1154
- 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/fork_map.rb +87 -0
- 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 +627 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
- 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 -821
- data/lib/rigor/inference/statement_evaluator.rb +754 -1114
- 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 +141 -184
- 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 +92 -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 +40 -48
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
- 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/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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/grape_api_discoverer.rb +189 -0
- 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 +61 -75
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
- 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/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +9 -1
|
@@ -7,49 +7,40 @@ require "rigor/source/literals"
|
|
|
7
7
|
module Rigor
|
|
8
8
|
module Plugin
|
|
9
9
|
class Actionpack < Rigor::Plugin::Base
|
|
10
|
-
# Per-node validators for the Action Pack DSL inside controller
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# filter / render phases need is read from the node-rule
|
|
17
|
-
# `NodeContext` ancestors rather than re-derived by a forward walk.
|
|
10
|
+
# Per-node validators for the Action Pack DSL inside controller files. ADR-37: the engine owns the
|
|
11
|
+
# single AST walk (the plugin declares `node_rule(Prism::CallNode)`), so each method here
|
|
12
|
+
# validates ONE call node and returns location-bearing {Violation}s — the `node_rule` block
|
|
13
|
+
# positions them via `Plugin::Base#diagnostic`. The enclosing-controller context the filter /
|
|
14
|
+
# render phases need is read from the node-rule `NodeContext` ancestors rather than re-derived by
|
|
15
|
+
# a forward walk.
|
|
18
16
|
#
|
|
19
17
|
# The recogniser keys on call-method-name suffix:
|
|
20
18
|
#
|
|
21
19
|
# - `users_path`, `edit_user_path(@user)` → `_path` family.
|
|
22
20
|
# - `users_url`, `edit_user_url(@user)` → `_url` family.
|
|
23
21
|
#
|
|
24
|
-
# Any call whose name doesn't end in `_path` / `_url` is
|
|
25
|
-
#
|
|
26
|
-
# receiver
|
|
27
|
-
# the helper is implicit-self in real controllers, and a
|
|
28
|
-
# custom-receiver call is almost certainly someone's own
|
|
22
|
+
# Any call whose name doesn't end in `_path` / `_url` is silently passed through. Calls with an
|
|
23
|
+
# explicit non-self receiver (`other_helper.users_path`) are also skipped — the helper is
|
|
24
|
+
# implicit-self in real controllers, and a custom-receiver call is almost certainly someone's own
|
|
29
25
|
# method that happens to share the suffix.
|
|
30
26
|
module Analyzer
|
|
31
27
|
SUFFIXES = %w[_path _url].freeze
|
|
32
28
|
|
|
33
|
-
# Phase 2 — filter-chain DSL methods. Each takes a
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# modifiers. Only the filter NAMES are validated; the
|
|
37
|
-
# `only:`/`except:` action-name arguments are not (deferred).
|
|
29
|
+
# Phase 2 — filter-chain DSL methods. Each takes a variadic list of filter names (Symbols /
|
|
30
|
+
# Strings) plus optional `only:` / `except:` / `if:` / `unless:` modifiers. Only the filter
|
|
31
|
+
# NAMES are validated; the `only:`/`except:` action-name arguments are not (deferred).
|
|
38
32
|
FILTER_DSL_METHODS = %i[
|
|
39
33
|
before_action after_action around_action
|
|
40
34
|
skip_before_action skip_after_action skip_around_action
|
|
41
35
|
prepend_before_action prepend_after_action prepend_around_action
|
|
42
36
|
].freeze
|
|
43
37
|
|
|
44
|
-
# Phase 3 — render-target template extensions checked in
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# extensions, the missing-template diagnostic stays silent.
|
|
51
|
-
# A configurable extension list is deferred; this set is wide
|
|
52
|
-
# enough to cover surveyed real-world projects without FPs.
|
|
38
|
+
# Phase 3 — render-target template extensions checked in priority order. Covers the engines used
|
|
39
|
+
# by surveyed projects: ERB (Rails default — `.html.erb`, `.text.erb`), HAML (Mastodon, Solidus
|
|
40
|
+
# admin — `.html.haml`), Slim, and JSON (`.json.jbuilder` plus `.json.erb` for hand-rolled API
|
|
41
|
+
# responses). When a template exists under any of these extensions, the missing-template
|
|
42
|
+
# diagnostic stays silent. A configurable extension list is deferred; this set is wide enough to
|
|
43
|
+
# cover surveyed real-world projects without FPs.
|
|
53
44
|
RENDER_TEMPLATE_EXTENSIONS = %w[
|
|
54
45
|
.html.erb
|
|
55
46
|
.text.erb
|
|
@@ -62,39 +53,37 @@ module Rigor
|
|
|
62
53
|
.xml.erb
|
|
63
54
|
].freeze
|
|
64
55
|
|
|
65
|
-
# Phase 1 — strong-parameter call shapes that begin a
|
|
66
|
-
#
|
|
67
|
-
# `params.require(:
|
|
68
|
-
# by looking for `:permit` call sites whose receiver is
|
|
69
|
-
# `params.require(:symbol)`.
|
|
56
|
+
# Phase 1 — strong-parameter call shapes that begin a validatable chain. The walker matches the
|
|
57
|
+
# `params.require(:user).permit(:name, :email)` chain by looking for `:permit` call sites whose
|
|
58
|
+
# receiver is `params.require(:symbol)`.
|
|
70
59
|
STRONG_PARAMS_RECEIVER_NAMES = %i[require permit_params strong_params].freeze
|
|
71
60
|
|
|
72
|
-
#
|
|
73
|
-
# (
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
# Max edit distance between a non-column permit key and a real column for the key to count as a
|
|
62
|
+
# typo (and fire `unknown-permit-key`) rather than a legitimate virtual attribute. A tight bound
|
|
63
|
+
# keeps genuine typos (`emial`→`email`, distance 2) while never firing on a virtual attribute,
|
|
64
|
+
# whose name is nothing like any column.
|
|
65
|
+
PERMIT_KEY_TYPO_MAX_DISTANCE = 2
|
|
66
|
+
|
|
67
|
+
# One Action Pack observation. Carries no path — the caller (the `node_rule` block) positions
|
|
68
|
+
# it via `Plugin::Base#diagnostic`. `location` is the Prism location the diagnostic should point
|
|
69
|
+
# at (a call's `message_loc` for the call itself, or a sub-argument's location for
|
|
70
|
+
# `unknown-*-key`-style diagnostics that point at the offending Symbol).
|
|
78
71
|
Violation = Data.define(:location, :message, :severity, :rule)
|
|
79
72
|
|
|
80
73
|
module_function
|
|
81
74
|
|
|
82
|
-
# Phase 4 — route-helper consumption. The helper-existence /
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
# error. Real-world impact pre-fix: ~301 duplicates on Mastodon and
|
|
91
|
-
# ~119 on Redmine, exactly stacked with the rails-routes
|
|
92
|
-
# diagnostics. Returns `[]` for unknown / arity-mismatch shapes.
|
|
75
|
+
# Phase 4 — route-helper consumption. The helper-existence / arity check for ONE call node.
|
|
76
|
+
# Only the **info-level** route resolution (`helper-call`) is the value this plugin adds — it
|
|
77
|
+
# surfaces the resolved HTTP method + path + action alongside the call site, which
|
|
78
|
+
# `rigor-rails-routes` does not. Unknown-helper and wrong-arity diagnostics are produced
|
|
79
|
+
# canonically by `rigor-rails-routes`'s own analyzer (same `:helper_table` source of truth), so
|
|
80
|
+
# emitting them here would double every call-site error. Real-world impact pre-fix: ~301
|
|
81
|
+
# duplicates on Mastodon and ~119 on Redmine, exactly stacked with the rails-routes diagnostics.
|
|
82
|
+
# Returns `[]` for unknown / arity-mismatch shapes.
|
|
93
83
|
#
|
|
94
84
|
# @param call_node [Prism::Node]
|
|
95
|
-
# @param helper_table [Hash{String => Hash}] each entry carries
|
|
96
|
-
# `
|
|
97
|
-
# `action`.
|
|
85
|
+
# @param helper_table [Hash{String => Hash}] each entry carries `name`, `arity`,
|
|
86
|
+
# `acceptable_arities`, `path`, `http_method`, `action`.
|
|
98
87
|
# @return [Array<Violation>]
|
|
99
88
|
def helper_violations_for(call_node:, helper_table:)
|
|
100
89
|
return [] unless implicit_helper_call?(call_node)
|
|
@@ -103,24 +92,20 @@ module Rigor
|
|
|
103
92
|
return [] if entry.nil?
|
|
104
93
|
|
|
105
94
|
actual_arity = positional_arg_count(call_node)
|
|
106
|
-
# The `:helper_table` fact entry carries both `:arity` (the
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# index/show). Honour the full set; fall back to the single
|
|
111
|
-
# arity for consumers compiled against an older fact shape.
|
|
95
|
+
# The `:helper_table` fact entry carries both `:arity` (the first registered entry's arity)
|
|
96
|
+
# and `:acceptable_arities` (the full Array — populated for uncountable-noun resources like
|
|
97
|
+
# `resources :news` which share a helper name across index/show). Honour the full set; fall
|
|
98
|
+
# back to the single arity for consumers compiled against an older fact shape.
|
|
112
99
|
acceptable = entry[:acceptable_arities] || [entry[:arity]]
|
|
113
100
|
return [] unless acceptable.include?(actual_arity)
|
|
114
101
|
|
|
115
102
|
[helper_call_violation(call_node, entry)]
|
|
116
103
|
end
|
|
117
104
|
|
|
118
|
-
# Phase 2 — filter-chain validation for ONE filter-DSL call
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# level of inheritance), and each filter name reference is
|
|
123
|
-
# validated against it. Calls outside a known controller
|
|
105
|
+
# Phase 2 — filter-chain validation for ONE filter-DSL call (`before_action :name`, …). The
|
|
106
|
+
# enclosing controller is read from the node-rule `NodeContext` ancestors, looked up in the
|
|
107
|
+
# controller index to get the effective method set (including one level of inheritance), and
|
|
108
|
+
# each filter name reference is validated against it. Calls outside a known controller
|
|
124
109
|
# contribute nothing.
|
|
125
110
|
#
|
|
126
111
|
# @param call_node [Prism::Node]
|
|
@@ -136,13 +121,11 @@ module Rigor
|
|
|
136
121
|
|
|
137
122
|
methods = controller_index.effective_methods_for(class_name)
|
|
138
123
|
spell_checker = DidYouMean::SpellChecker.new(dictionary: methods.map(&:to_s))
|
|
139
|
-
# When the controller (or its parent) `include`s a module the
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# `
|
|
143
|
-
#
|
|
144
|
-
# `unknown-filter-method` in that case rather than FPing on
|
|
145
|
-
# legitimate gem-provided callback names.
|
|
124
|
+
# When the controller (or its parent) `include`s a module the discoverer couldn't resolve —
|
|
125
|
+
# typically a gem-shipped concern such as `Devise::Controllers::Helpers` or
|
|
126
|
+
# `Pundit::Authorization` — any `before_action :name` MIGHT be defined in that unresolved
|
|
127
|
+
# module. Suppress `unknown-filter-method` in that case rather than FPing on legitimate
|
|
128
|
+
# gem-provided callback names.
|
|
146
129
|
ambiguous_filters = controller_index.unresolved_include?(class_name)
|
|
147
130
|
|
|
148
131
|
filter_name_args(call_node).filter_map do |arg_node|
|
|
@@ -156,12 +139,10 @@ module Rigor
|
|
|
156
139
|
end
|
|
157
140
|
end
|
|
158
141
|
|
|
159
|
-
# Phase 1 — strong-parameter validation for ONE `permit` call.
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# `rigor-activerecord`). Calls whose `:require` argument is a
|
|
164
|
-
# non-literal Symbol are passed through; namespaced models
|
|
142
|
+
# Phase 1 — strong-parameter validation for ONE `permit` call. Recognises the
|
|
143
|
+
# `params.require(:symbol).permit(:key, :key, ...)` chain and validates each `:key` against the
|
|
144
|
+
# AR model's column list (looked up via the model_index fact published by `rigor-activerecord`).
|
|
145
|
+
# Calls whose `:require` argument is a non-literal Symbol are passed through; namespaced models
|
|
165
146
|
# (`params.require(:admin_user)` → `Admin::User`) are deferred.
|
|
166
147
|
#
|
|
167
148
|
# @param call_node [Prism::Node]
|
|
@@ -175,33 +156,28 @@ module Rigor
|
|
|
175
156
|
return [] if entry.nil? # unknown model — skip; the model lookup is best-effort.
|
|
176
157
|
|
|
177
158
|
columns = entry[:columns]
|
|
178
|
-
|
|
179
|
-
literal_permit_keys(call_node).map do |key_node, key_name|
|
|
159
|
+
literal_permit_keys(call_node).filter_map do |key_node, key_name|
|
|
180
160
|
if columns.include?(key_name)
|
|
181
161
|
permit_call_violation(call_node, model_class, key_name)
|
|
182
162
|
else
|
|
183
|
-
unknown_permit_key_violation(key_node, model_class, key_name,
|
|
163
|
+
unknown_permit_key_violation(key_node, model_class, key_name, columns)
|
|
184
164
|
end
|
|
185
165
|
end
|
|
186
166
|
end
|
|
187
167
|
|
|
188
|
-
# Phase 3 — render-target validation for ONE `render` call. Derive
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# configured `view_search_paths` (default `["app/views"]`).
|
|
192
|
-
# Recognised call shapes:
|
|
168
|
+
# Phase 3 — render-target validation for ONE `render` call. Derive the candidate view template
|
|
169
|
+
# path(s) from the controller class name + the render argument shape, then check existence
|
|
170
|
+
# under the configured `view_search_paths` (default `["app/views"]`). Recognised call shapes:
|
|
193
171
|
#
|
|
194
172
|
# - `render :symbol` — `<views>/<controller_path>/<symbol>.html.erb`
|
|
195
173
|
# - `render "string/path"` — `<views>/<string_path>.html.erb`
|
|
196
174
|
# - `render partial: "name"` — `<views>/<controller_path>/_<name>.html.erb`
|
|
197
175
|
# - `render partial: "string/path"` — `<views>/<string_path with _ prefix>.html.erb`
|
|
198
176
|
#
|
|
199
|
-
# `render layout:`, `render plain:`, `render json:`, `render
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# validates explicit renders only, since the implicit path would
|
|
204
|
-
# false-positive on `redirect_to` / `head` / early returns.
|
|
177
|
+
# `render layout:`, `render plain:`, `render json:`, `render text:`, `render inline:`, `render
|
|
178
|
+
# :nothing => true`, etc. are pass-through (no template lookup). Implicit-render (a controller
|
|
179
|
+
# method that doesn't call `render`) is also skipped — Phase 3 validates explicit renders only,
|
|
180
|
+
# since the implicit path would false-positive on `redirect_to` / `head` / early returns.
|
|
205
181
|
#
|
|
206
182
|
# @param call_node [Prism::Node]
|
|
207
183
|
# @param ancestors [Array<Prism::Node>] the lexical ancestor chain
|
|
@@ -215,41 +191,32 @@ module Rigor
|
|
|
215
191
|
class_name = enclosing_controller_name(ancestors)
|
|
216
192
|
return [] if class_name.nil?
|
|
217
193
|
|
|
218
|
-
# Prefer the file-path-derived controller path when the source
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
# `Users
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
# disambiguator Rails picks). Fall back to the AST-derived class
|
|
225
|
-
# name for files outside `app/controllers/` (libraries, test
|
|
226
|
-
# fixtures).
|
|
194
|
+
# Prefer the file-path-derived controller path when the source lives under `app/controllers/`
|
|
195
|
+
# — Rails autoload is the runtime authority on `Admin::Users::RolesController` vs
|
|
196
|
+
# `Users::RolesController` (a declaration like `module Admin; class Users::RolesController`
|
|
197
|
+
# resolves to whichever `Users` constant Ruby finds first, and the file path is the
|
|
198
|
+
# disambiguator Rails picks). Fall back to the AST-derived class name for files outside
|
|
199
|
+
# `app/controllers/` (libraries, test fixtures).
|
|
227
200
|
controller_path = controller_path_from_file(path) || controller_path_for(class_name)
|
|
228
201
|
return [] if controller_path.nil?
|
|
229
202
|
|
|
230
|
-
# Render checks silence when the controller (or its ancestor
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
# views; the local subclass calling `render :show` resolves
|
|
235
|
-
# through the gem's view path, which our static analyser doesn't
|
|
236
|
-
# know about.
|
|
203
|
+
# Render checks silence when the controller (or its ancestor chain) inherits from a
|
|
204
|
+
# gem-shipped parent (Devise::ConfirmationsController, Doorkeeper::ApplicationsController, …).
|
|
205
|
+
# The gem ships its own views; the local subclass calling `render :show` resolves through the
|
|
206
|
+
# gem's view path, which our static analyser doesn't know about.
|
|
237
207
|
if controller_index&.known?(class_name) &&
|
|
238
208
|
controller_index.unresolved_include?(class_name)
|
|
239
209
|
return []
|
|
240
210
|
end
|
|
241
211
|
|
|
242
|
-
# Abstract base controllers: a `*BaseController` (`Admin::
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
# … but no top-level templates). Skip render checks for these —
|
|
251
|
-
# the diagnostic would be a false positive against intentional
|
|
252
|
-
# Rails abstract-base layouts.
|
|
212
|
+
# Abstract base controllers: a `*BaseController` (`Admin::BaseController`,
|
|
213
|
+
# `Settings::Preferences::BaseController`, …) typically has no view of its own; its `render
|
|
214
|
+
# :show` bodies are resolved by Rails against the calling subclass's controller path at
|
|
215
|
+
# request time, NOT the base's. Same for parent controllers whose view directory exists but
|
|
216
|
+
# contains only subdirectories (Mastodon's `Admin::SettingsController` whose
|
|
217
|
+
# `app/views/admin/settings/` holds about/, appearance/, … but no top-level templates). Skip
|
|
218
|
+
# render checks for these — the diagnostic would be a false positive against intentional Rails
|
|
219
|
+
# abstract-base layouts.
|
|
253
220
|
return [] if abstract_base_controller?(class_name, controller_path, view_search_roots)
|
|
254
221
|
|
|
255
222
|
target = render_target_for(call_node, controller_path)
|
|
@@ -259,27 +226,22 @@ module Rigor
|
|
|
259
226
|
violation ? [violation] : []
|
|
260
227
|
end
|
|
261
228
|
|
|
262
|
-
# Resolves the qualified name of the controller class the node
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
# root and validated nested-class call sites against it. Enclosing
|
|
268
|
-
# `ModuleNode`s above it contribute the namespace, so the
|
|
269
|
-
# nested-module declaration shape
|
|
229
|
+
# Resolves the qualified name of the controller class the node sits in from its lexical
|
|
230
|
+
# ancestors (the node-rule `NodeContext#ancestors`, outermost first). The OUTERMOST `ClassNode`
|
|
231
|
+
# is the controller — matching the old forward walk, which returned the first `ClassNode`
|
|
232
|
+
# reachable from the file root and validated nested-class call sites against it. Enclosing
|
|
233
|
+
# `ModuleNode`s above it contribute the namespace, so the nested-module declaration shape
|
|
270
234
|
#
|
|
271
235
|
# module Admin
|
|
272
236
|
# class DomainBlocksController < BaseController
|
|
273
237
|
# end
|
|
274
238
|
# end
|
|
275
239
|
#
|
|
276
|
-
# is recovered as `Admin::DomainBlocksController` — the same name
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
# `
|
|
280
|
-
#
|
|
281
|
-
# (Mastodon's `admin/domain_blocks` rendered as bare
|
|
282
|
-
# `domain_blocks`). Returns nil when no enclosing class exists.
|
|
240
|
+
# is recovered as `Admin::DomainBlocksController` — the same name the `ControllerDiscoverer`
|
|
241
|
+
# registers under. Without this, a bare inner-class name would be used for index lookups +
|
|
242
|
+
# `controller_path_for`, silently skipping filter validation and pointing render template paths
|
|
243
|
+
# at the wrong directory (Mastodon's `admin/domain_blocks` rendered as bare `domain_blocks`).
|
|
244
|
+
# Returns nil when no enclosing class exists.
|
|
283
245
|
def enclosing_controller_name(ancestors)
|
|
284
246
|
class_index = ancestors.index { |n| n.is_a?(Prism::ClassNode) }
|
|
285
247
|
return nil if class_index.nil?
|
|
@@ -296,10 +258,9 @@ module Rigor
|
|
|
296
258
|
path ? path.split("::") : []
|
|
297
259
|
end
|
|
298
260
|
|
|
299
|
-
# Resolves a class-name AST node against an enclosing namespace
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
# `class Foo` inside `module Admin`) is qualified against it.
|
|
261
|
+
# Resolves a class-name AST node against an enclosing namespace chain. A `ConstantPathNode`
|
|
262
|
+
# (e.g. `class Admin::Foo`) is already absolute and ignores the chain; a `ConstantReadNode`
|
|
263
|
+
# (bare `class Foo` inside `module Admin`) is qualified against it.
|
|
303
264
|
def qualified_name_with_enclosing(node, enclosing)
|
|
304
265
|
return nil unless node.is_a?(Prism::Node)
|
|
305
266
|
|
|
@@ -334,9 +295,8 @@ module Rigor
|
|
|
334
295
|
node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :render
|
|
335
296
|
end
|
|
336
297
|
|
|
337
|
-
# Drops the trailing keyword hash (`only:` / `except:` / `if:` /
|
|
338
|
-
#
|
|
339
|
-
# names.
|
|
298
|
+
# Drops the trailing keyword hash (`only:` / `except:` / `if:` / `unless:`) so the modifier args
|
|
299
|
+
# don't get treated as filter names.
|
|
340
300
|
def filter_name_args(call_node)
|
|
341
301
|
args = call_node.arguments&.arguments || []
|
|
342
302
|
args = args[0..-2] if args.last.is_a?(Prism::KeywordHashNode)
|
|
@@ -351,9 +311,8 @@ module Rigor
|
|
|
351
311
|
if methods.include?(filter_name.to_sym)
|
|
352
312
|
filter_call_violation(call_node, filter_name)
|
|
353
313
|
elsif ambiguous_filters
|
|
354
|
-
# An unresolved include shadows our judgment — emit the
|
|
355
|
-
#
|
|
356
|
-
# indexed, but skip the error.
|
|
314
|
+
# An unresolved include shadows our judgment — emit the recognized-filter info anyway so
|
|
315
|
+
# the call site is still indexed, but skip the error.
|
|
357
316
|
filter_call_violation(call_node, filter_name)
|
|
358
317
|
else
|
|
359
318
|
unknown_filter_violation(arg_node, call_node, filter_name, spell_checker)
|
|
@@ -373,11 +332,9 @@ module Rigor
|
|
|
373
332
|
def model_class_for_permit(permit_call)
|
|
374
333
|
require_call = permit_call.receiver
|
|
375
334
|
symbol_node = require_call.arguments.arguments.first
|
|
376
|
-
# `:user` → `User`, `:order_item` → `OrderItem`. ADR-39: the
|
|
377
|
-
#
|
|
378
|
-
# `
|
|
379
|
-
# case the former `split("_").map(&:capitalize)` only
|
|
380
|
-
# approximated is authoritative), falling back otherwise.
|
|
335
|
+
# `:user` → `User`, `:order_item` → `OrderItem`. ADR-39: the shared inflector camelizes
|
|
336
|
+
# through the real `ActiveSupport::Inflector` when present (so the multi-word case the former
|
|
337
|
+
# `split("_").map(&:capitalize)` only approximated is authoritative), falling back otherwise.
|
|
381
338
|
Rigor::Plugin::Inflector.camelize(symbol_node.value.to_s)
|
|
382
339
|
end
|
|
383
340
|
|
|
@@ -395,39 +352,44 @@ module Rigor
|
|
|
395
352
|
)
|
|
396
353
|
end
|
|
397
354
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
355
|
+
# A permit key that is not a column is only a violation when it is a near-miss TYPO of a real
|
|
356
|
+
# column. Permitting a non-column is ordinary Rails: a Devise virtual attribute (`password`,
|
|
357
|
+
# `remember_me`, `otp_attempt`), a state-machine `*_event`, an `attr_accessor` setter, or a nested
|
|
358
|
+
# `*_attributes` key are all mass-assignable but absent from the schema. So fire only when a column
|
|
359
|
+
# sits within a tight edit distance of the key (a typo like `emial`→`email`); a key with no close
|
|
360
|
+
# column is presumed a deliberate virtual attribute and passes silently. Firing on every non-column
|
|
361
|
+
# key false-positives across the whole strong-params surface of a Devise/state-machine app.
|
|
362
|
+
def unknown_permit_key_violation(key_node, model_class, key_name, columns)
|
|
363
|
+
nearest = columns.min_by { |col| DidYouMean::Levenshtein.distance(key_name, col) }
|
|
364
|
+
return nil if nearest.nil?
|
|
365
|
+
return nil if DidYouMean::Levenshtein.distance(key_name, nearest) > PERMIT_KEY_TYPO_MAX_DISTANCE
|
|
366
|
+
|
|
402
367
|
Violation.new(
|
|
403
368
|
location: key_node.location,
|
|
404
|
-
message:
|
|
369
|
+
message: "Action Pack permit `#{key_name}` is not a column on `#{model_class}`. " \
|
|
370
|
+
"Did you mean `:#{nearest}`?",
|
|
371
|
+
severity: :error, rule: "unknown-permit-key"
|
|
405
372
|
)
|
|
406
373
|
end
|
|
407
374
|
|
|
408
|
-
# Convert `<root>/app/controllers/admin/users/roles_controller.rb`
|
|
409
|
-
#
|
|
410
|
-
# `app/controllers/` segment.
|
|
375
|
+
# Convert `<root>/app/controllers/admin/users/roles_controller.rb` to `admin/users/roles`.
|
|
376
|
+
# Returns nil if the path is not under an `app/controllers/` segment.
|
|
411
377
|
def controller_path_from_file(path)
|
|
412
378
|
match = path.match(%r{(?:\A|/)app/controllers/(.+)_controller\.rb\z})
|
|
413
379
|
match&.[](1)
|
|
414
380
|
end
|
|
415
381
|
|
|
416
|
-
# An abstract base controller — its `render` bodies don't validate
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
# subdirectories (no template files at its top). Mastodon's
|
|
425
|
-
# Admin::SettingsController whose app/views/admin/settings/
|
|
426
|
-
# holds only about/, appearance/, … — the parent of nested
|
|
382
|
+
# An abstract base controller — its `render` bodies don't validate against its own
|
|
383
|
+
# controller_path because Rails resolves at request time against the actual subclass. Two
|
|
384
|
+
# conservative heuristics:
|
|
385
|
+
# (1) The class name ends with `BaseController`. Strong Rails-convention signal
|
|
386
|
+
# (Settings::Preferences::BaseController, Admin::BaseController, …).
|
|
387
|
+
# (2) The controller's view directory exists but contains ONLY subdirectories (no template
|
|
388
|
+
# files at its top). Mastodon's Admin::SettingsController whose
|
|
389
|
+
# app/views/admin/settings/ holds only about/, appearance/, … — the parent of nested
|
|
427
390
|
# controllers.
|
|
428
|
-
# Deliberately NOT triggered by "no view directory at all" because
|
|
429
|
-
#
|
|
430
|
-
# (the typo / forgot-to-create case).
|
|
391
|
+
# Deliberately NOT triggered by "no view directory at all" because that fires the diagnostic we
|
|
392
|
+
# DO want for genuinely-missing views (the typo / forgot-to-create case).
|
|
431
393
|
def abstract_base_controller?(class_name, controller_path, view_search_roots)
|
|
432
394
|
return true if class_name.end_with?("BaseController")
|
|
433
395
|
|
|
@@ -440,12 +402,10 @@ module Rigor
|
|
|
440
402
|
end
|
|
441
403
|
end
|
|
442
404
|
|
|
443
|
-
# Returns `[kind, view_relative_path]` where kind is `:template`
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
# validate (`layout:` / `plain:` / `json:` / `text:` / `inline:` /
|
|
448
|
-
# `:nothing` / no parseable target).
|
|
405
|
+
# Returns `[kind, view_relative_path]` where kind is `:template` or `:partial`, and
|
|
406
|
+
# view_relative_path is the path under view_search_roots WITHOUT extension (the extension
|
|
407
|
+
# family is appended at lookup time). Returns nil for shapes Phase 3 doesn't validate (`layout:`
|
|
408
|
+
# / `plain:` / `json:` / `text:` / `inline:` / `:nothing` / no parseable target).
|
|
449
409
|
def render_target_for(call_node, controller_path)
|
|
450
410
|
args = call_node.arguments&.arguments || []
|
|
451
411
|
return nil if args.empty?
|
|
@@ -454,9 +414,8 @@ module Rigor
|
|
|
454
414
|
# `render partial: "..."` — the keyword form.
|
|
455
415
|
return partial_target_from_kwargs(first, controller_path) if first.is_a?(Prism::KeywordHashNode)
|
|
456
416
|
|
|
457
|
-
# `render :symbol` / `render "path"`. A trailing KeywordHashNode
|
|
458
|
-
#
|
|
459
|
-
# positional carries the template name.
|
|
417
|
+
# `render :symbol` / `render "path"`. A trailing KeywordHashNode is allowed (e.g. `render
|
|
418
|
+
# :show, status: :ok`); the leading positional carries the template name.
|
|
460
419
|
template_target_from_positional(first, controller_path)
|
|
461
420
|
end
|
|
462
421
|
|
|
@@ -487,15 +446,13 @@ module Rigor
|
|
|
487
446
|
|
|
488
447
|
# `UsersController` → "users".
|
|
489
448
|
# `Admin::WidgetsController` → "admin/widgets".
|
|
490
|
-
# Returns nil for class names that don't end with the `Controller`
|
|
491
|
-
# suffix.
|
|
449
|
+
# Returns nil for class names that don't end with the `Controller` suffix.
|
|
492
450
|
def controller_path_for(class_name)
|
|
493
451
|
return nil unless class_name.end_with?("Controller")
|
|
494
452
|
|
|
495
453
|
stripped = class_name.delete_suffix("Controller")
|
|
496
|
-
# ADR-39: the shared inflector flattens `::` → `/` and applies
|
|
497
|
-
#
|
|
498
|
-
# produced the same result for the common case but missed
|
|
454
|
+
# ADR-39: the shared inflector flattens `::` → `/` and applies Rails' real underscore (the
|
|
455
|
+
# per-segment hand-rolled version produced the same result for the common case but missed
|
|
499
456
|
# acronym handling). `Admin::Widgets` → `admin/widgets`.
|
|
500
457
|
Rigor::Plugin::Inflector.underscore(stripped)
|
|
501
458
|
end
|