rigortype 0.2.8 → 0.3.0
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 +2 -2
- 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/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- 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/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -122,6 +122,16 @@ module Rigor
|
|
|
122
122
|
carried_type(receiver_type, call_node.name)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# ADR-88 WD1 — the `dynamic_return` contribution is a PURE STRUCTURAL projection of the receiver's own
|
|
126
|
+
# carrier type args (`Result[T, E]#unwrap → T`), computed per call from the receiver type the engine
|
|
127
|
+
# already resolved; it reads no cross-file catalog. (The `Enum` nested-class emission rides the ADR-16
|
|
128
|
+
# macro substrate / synthetic-method scanner, a separate pre-pass, not this `dynamic_return`.) A stable
|
|
129
|
+
# sentinel declares "no cross-file fact surface", keeping the plugin incremental-capable; the
|
|
130
|
+
# `--verify-incremental` gate backstops the claim.
|
|
131
|
+
def incremental_state_fingerprint
|
|
132
|
+
"structural-carriers"
|
|
133
|
+
end
|
|
134
|
+
|
|
125
135
|
private
|
|
126
136
|
|
|
127
137
|
# @return [Rigor::Type, nil] the receiver's inferred type, or nil when the engine raises on a synthetic
|
|
@@ -62,6 +62,17 @@ module Rigor
|
|
|
62
62
|
narrowing_facts methods: AssertionAnalyzer::SUPPORTED_METHODS do |call_node, scope|
|
|
63
63
|
AssertionAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
|
|
64
64
|
end
|
|
65
|
+
|
|
66
|
+
# ADR-88 WD1 — the narrowing facts this plugin contributes are derived purely from each assertion call's
|
|
67
|
+
# own AST at its call site (`assert_kind_of(String, x)` ⇒ `x` is `String` on the continuation); there is
|
|
68
|
+
# no cross-file catalog a cached diagnostic could depend on. A change to a file's assertions changes that
|
|
69
|
+
# file's own content (re-analysed by the incremental graph already). A stable sentinel declares "no
|
|
70
|
+
# cross-file fact surface", keeping the plugin incremental-capable (a contributing plugin with NO
|
|
71
|
+
# fact / producer / hook makes the incremental snapshot un-reusable every run); `--verify-incremental`
|
|
72
|
+
# backstops the claim.
|
|
73
|
+
def incremental_state_fingerprint
|
|
74
|
+
"per-file-assertions"
|
|
75
|
+
end
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
Rigor::Plugin.register(Minitest)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
require_relative "policy_index"
|
|
@@ -69,7 +71,7 @@ module Rigor
|
|
|
69
71
|
when Prism::ClassNode then visit_class(node, lexical_path, &)
|
|
70
72
|
when Prism::ModuleNode then visit_module(node, lexical_path, &)
|
|
71
73
|
else
|
|
72
|
-
node.
|
|
74
|
+
node.rigor_each_child { |child| walk_for_policies(child, lexical_path, &) }
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# have to list seven require lines.
|
|
3
|
+
# The Tier 1+2 Rails set, as a single require. Requiring this file registers every member class with
|
|
4
|
+
# `Rigor::Plugin`; the loader's lookup phase then finds them by id when `.rigor.yml` enumerates them.
|
|
6
5
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# UNWIRED — this entry point has no supported use today, and is kept deliberately (ADR-96 WD5) rather
|
|
7
|
+
# than removed. It was ADR-12 WD1's "Gemfile-convenience meta-gem": each plugin was to be its own gem, and
|
|
8
|
+
# `gem "rigor-rails"` was to pull the Rails set into a project's Gemfile in one line. Commit `9769f5fa`
|
|
9
|
+
# dropped the per-plugin gemspecs and ADR-31 settled distribution on the single bundled `rigortype` gem, so
|
|
10
|
+
# there is no `rigor-rails` gem to add to a Gemfile — and `plugins: [rigor-rails]` is rejected by the
|
|
11
|
+
# loader, which requires the members to be enumerated (or one selected with an explicit `id:`).
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# Adding the gem to a project's Gemfile without listing any plugin in `.rigor.yml` is harmless: the requires
|
|
16
|
-
# happen on `Bundler.require`, but no plugin's `init` / `prepare` / hooks run.
|
|
13
|
+
# ADR-96 WD3 proposes the role this file would fill: `plugins: [rigor-rails]` activating those members
|
|
14
|
+
# whose `target_gems:` are actually in the project's `Gemfile.lock`. If that is rejected, ADR-60 WD1's
|
|
15
|
+
# never-wired-surface criterion applies and this directory goes.
|
|
17
16
|
|
|
18
17
|
require "rigor-rails-routes"
|
|
19
18
|
require "rigor-rails-i18n"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
5
|
+
require "prism"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Plugin
|
|
9
|
+
class RailsRoutes < Rigor::Plugin::Base
|
|
10
|
+
# Recognises the helper *namespaces* the `grape-path-helpers` gem generates, so a call to one of them
|
|
11
|
+
# does not false-fire `unknown-helper`.
|
|
12
|
+
#
|
|
13
|
+
# The gem names each helper after its route's path segments, joined with `_`
|
|
14
|
+
# (`DecoratedRoute#path_helper_name`): `/api/:version/groups/:id/badges` under `version 'v4'` becomes
|
|
15
|
+
# `api_v4_groups_badges_path`. It walks `Grape::API::Instance.routes` — the *runtime* route table — and
|
|
16
|
+
# real grape sources build that table with metaprogramming (GitLab's `%w[group project].each { |t|
|
|
17
|
+
# resource t.pluralize … }`). A static parser cannot enumerate those names, and deriving only the
|
|
18
|
+
# easy ones would be worse than deriving none: every route it missed would keep firing on working
|
|
19
|
+
# code.
|
|
20
|
+
#
|
|
21
|
+
# What IS static is the leading segments. `prefix :api` contributes the first; `version 'v4'`
|
|
22
|
+
# contributes the second when the strategy is `:path` (Grape's default — `using: :header` / `:param`
|
|
23
|
+
# keeps the version out of the URL, and so out of the helper name). Both are literal arguments in the
|
|
24
|
+
# body of a class whose superclass chain reaches `Grape::API`. Everything after them is opaque.
|
|
25
|
+
#
|
|
26
|
+
# So the namespace `api_v4_…_path` is treated as open — a name the project's routes may define but
|
|
27
|
+
# Rigor cannot enumerate, so proving it undefined is unsound (the ADR-26 `open_receivers` reasoning,
|
|
28
|
+
# already applied in this plugin to Devise's runtime-provider OmniAuth family). Teeth survive where the
|
|
29
|
+
# gem's own contract keeps them sound: it defines no `_url` helper
|
|
30
|
+
# (`NamedRouteMatcher#method_missing` returns `super` unless the name ends `_path`), so
|
|
31
|
+
# `api_v4_anything_url` still fires, as does any name outside a declared prefix.
|
|
32
|
+
#
|
|
33
|
+
# Design note: `docs/notes/20260710-grape-path-helper-namespace.md`.
|
|
34
|
+
module GrapeApiDiscoverer
|
|
35
|
+
# A class is a grape API when its superclass chain reaches one of these. `Grape::API::Instance` is
|
|
36
|
+
# what a mounted `Grape::API` subclass actually becomes, and real code inherits from it directly
|
|
37
|
+
# (GitLab's `API::Base < Grape::API::Instance`).
|
|
38
|
+
GRAPE_BASE_NAMES = ["Grape::API", "Grape::API::Instance"].freeze
|
|
39
|
+
|
|
40
|
+
# `version 'v4', using: :path` puts the version in the URL. `:header` / `:param` do not, so those
|
|
41
|
+
# versions contribute no helper-name segment. Grape's default strategy is `:path`.
|
|
42
|
+
PATH_VERSION_STRATEGY = :path
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# @param contents_per_path [Hash{String => String}] file path → source text, read by the caller
|
|
47
|
+
# (through the trusted `IoBoundary`, so cache invalidation works).
|
|
48
|
+
# @return [Array<String>] the recognised helper-name prefixes (`["api_v3", "api_v4"]`). Empty when
|
|
49
|
+
# the project declares no grape API — nothing changes for such a project.
|
|
50
|
+
def discover(contents_per_path)
|
|
51
|
+
declarations = {}
|
|
52
|
+
superclasses = {}
|
|
53
|
+
contents_per_path.each_value do |contents|
|
|
54
|
+
collect_from_contents(contents, declarations, superclasses)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
# Best-effort: a parse failure in one grape file must not abort discovery for the rest. The core
|
|
57
|
+
# pipeline surfaces the parse error separately.
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
prefixes_for(declarations, superclasses)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Composes `prefix` × `version` into helper-name prefixes, for grape classes only. A class with a
|
|
64
|
+
# prefix and no path-strategy version contributes the bare prefix; one with versions and no prefix
|
|
65
|
+
# contributes each version alone.
|
|
66
|
+
def prefixes_for(declarations, superclasses)
|
|
67
|
+
declarations.filter_map do |class_name, declaration|
|
|
68
|
+
next unless grape_api?(class_name, superclasses)
|
|
69
|
+
|
|
70
|
+
segments = declaration[:prefix]
|
|
71
|
+
versions = declaration[:versions]
|
|
72
|
+
next segments.join("_") if versions.empty? && !segments.empty?
|
|
73
|
+
|
|
74
|
+
versions.map { |version| (segments + [version]).join("_") }
|
|
75
|
+
end.flatten.reject(&:empty?).uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Follows the recorded superclass chain (cycle-guarded) looking for a grape base. The chain lives
|
|
79
|
+
# entirely inside the scanned files, so an unresolvable parent simply ends the walk.
|
|
80
|
+
def grape_api?(class_name, superclasses)
|
|
81
|
+
seen = {}
|
|
82
|
+
current = class_name
|
|
83
|
+
while current && !seen[current]
|
|
84
|
+
seen[current] = true
|
|
85
|
+
parent = superclasses[current]
|
|
86
|
+
return true if parent && GRAPE_BASE_NAMES.include?(parent)
|
|
87
|
+
|
|
88
|
+
current = parent
|
|
89
|
+
end
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def collect_from_contents(contents, declarations, superclasses)
|
|
94
|
+
root = Prism.parse(contents).value
|
|
95
|
+
walk(root, [], declarations, superclasses)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def walk(node, prefix_path, declarations, superclasses)
|
|
99
|
+
return unless node.is_a?(Prism::Node)
|
|
100
|
+
|
|
101
|
+
if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
102
|
+
name = constant_name(node.constant_path)
|
|
103
|
+
if name
|
|
104
|
+
qualified = (prefix_path + [name]).join("::")
|
|
105
|
+
record_class(node, qualified, declarations, superclasses) if node.is_a?(Prism::ClassNode)
|
|
106
|
+
walk(node.body, prefix_path + [name], declarations, superclasses) if node.body
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
node.rigor_each_child { |child| walk(child, prefix_path, declarations, superclasses) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def record_class(node, qualified, declarations, superclasses)
|
|
115
|
+
parent = constant_name(node.superclass)
|
|
116
|
+
superclasses[qualified] = parent if parent
|
|
117
|
+
declaration = { prefix: [], versions: [] }
|
|
118
|
+
statements_of(node.body).each { |statement| absorb_declaration(statement, declaration) }
|
|
119
|
+
declarations[qualified] = declaration unless declaration[:prefix].empty? && declaration[:versions].empty?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `prefix :api` / `prefix 'api/v2'` (a multi-segment prefix splits on `/`), and `version 'v4'` /
|
|
123
|
+
# `version 'v4', using: :path` — both in block and non-block form.
|
|
124
|
+
def absorb_declaration(node, declaration)
|
|
125
|
+
return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
126
|
+
|
|
127
|
+
case node.name
|
|
128
|
+
when :prefix
|
|
129
|
+
value = literal_value(first_argument(node))
|
|
130
|
+
declaration[:prefix] = value.split("/").reject(&:empty?) if value
|
|
131
|
+
when :version
|
|
132
|
+
value = literal_value(first_argument(node))
|
|
133
|
+
declaration[:versions] << value if value && path_strategy?(node)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# True unless an explicit `using:` names a non-path strategy. Grape defaults to `:path`.
|
|
138
|
+
def path_strategy?(node)
|
|
139
|
+
arguments = node.arguments&.arguments || []
|
|
140
|
+
keywords = arguments.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
141
|
+
return true if keywords.nil?
|
|
142
|
+
|
|
143
|
+
using = keywords.elements.find do |element|
|
|
144
|
+
element.is_a?(Prism::AssocNode) && literal_value(element.key) == "using"
|
|
145
|
+
end
|
|
146
|
+
return true if using.nil?
|
|
147
|
+
|
|
148
|
+
literal_value(using.value) == PATH_VERSION_STRATEGY.to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def first_argument(node)
|
|
152
|
+
node.arguments&.arguments&.first
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Symbol and String literals only. A computed prefix / version is not a declaration we can ground a
|
|
156
|
+
# namespace in, so it contributes nothing.
|
|
157
|
+
def literal_value(node)
|
|
158
|
+
case node
|
|
159
|
+
when Prism::SymbolNode, Prism::StringNode then node.unescaped
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def constant_name(node)
|
|
164
|
+
case node
|
|
165
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
166
|
+
when Prism::ConstantPathNode then constant_path_name(node)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def constant_path_name(node)
|
|
171
|
+
parent = node.parent
|
|
172
|
+
own = node.name&.to_s
|
|
173
|
+
return nil unless own
|
|
174
|
+
return own if parent.nil? # `::Foo` — a cbase root
|
|
175
|
+
|
|
176
|
+
parent_name = constant_name(parent)
|
|
177
|
+
parent_name ? "#{parent_name}::#{own}" : own
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def statements_of(body)
|
|
181
|
+
case body
|
|
182
|
+
when Prism::StatementsNode then body.body
|
|
183
|
+
when Prism::BeginNode then statements_of(body.statements)
|
|
184
|
+
when nil then []
|
|
185
|
+
else [body]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -87,7 +89,7 @@ module Rigor
|
|
|
87
89
|
def visit_program_children(node)
|
|
88
90
|
return unless node.respond_to?(:compact_child_nodes)
|
|
89
91
|
|
|
90
|
-
node.
|
|
92
|
+
node.rigor_each_child do |child|
|
|
91
93
|
case child
|
|
92
94
|
when Prism::ModuleNode, Prism::ClassNode
|
|
93
95
|
visit_module_or_class(child)
|
|
@@ -36,7 +36,11 @@ module Rigor
|
|
|
36
36
|
# (`<resource>_<provider>_omniauth_(authorize|callback)_(path|url)`) whose provider segment is
|
|
37
37
|
# supplied at runtime — no per-name entry is registered for them but they MUST NOT fire
|
|
38
38
|
# `unknown-helper` either.
|
|
39
|
-
|
|
39
|
+
# @param grape_prefixes [Enumerable<String>] helper-name prefixes generated by `grape-path-helpers`
|
|
40
|
+
# (`"api_v4"`), composed by {GrapeApiDiscoverer} from the project's own `prefix` / `version`
|
|
41
|
+
# declarations. The names beyond the prefix come from grape's runtime route table and cannot be
|
|
42
|
+
# enumerated statically, so the namespace is open — same contract as the OmniAuth family above.
|
|
43
|
+
def initialize(entries, custom_helpers: [], devise_resources: [], grape_prefixes: [])
|
|
40
44
|
@entries = entries.freeze
|
|
41
45
|
# Multimap: a single helper name can map to multiple entries when an uncountable-noun resource
|
|
42
46
|
# registers both an arity-0 index helper and an arity-1 show helper under the same `news_path`
|
|
@@ -45,6 +49,7 @@ module Rigor
|
|
|
45
49
|
@by_name = entries.group_by(&:name).transform_values(&:freeze).freeze
|
|
46
50
|
@custom_helpers = custom_helpers.to_set.freeze
|
|
47
51
|
@devise_resources = devise_resources.to_set(&:to_s).freeze
|
|
52
|
+
@grape_prefixes = grape_prefixes.to_set(&:to_s).freeze
|
|
48
53
|
freeze
|
|
49
54
|
end
|
|
50
55
|
|
|
@@ -67,10 +72,23 @@ module Rigor
|
|
|
67
72
|
name = helper_name.to_s
|
|
68
73
|
return true if @by_name.key?(name)
|
|
69
74
|
return true if @custom_helpers.include?(name)
|
|
75
|
+
return true if grape_match?(name)
|
|
70
76
|
|
|
71
77
|
omniauth_match?(name)
|
|
72
78
|
end
|
|
73
79
|
|
|
80
|
+
# `<prefix>_<anything>_path` for a prefix the project's grape API declares. Only the `_path` suffix
|
|
81
|
+
# matches: `grape-path-helpers` defines no `_url` helper (its `method_missing` returns `super` unless
|
|
82
|
+
# the name ends with `_path`), so `api_v4_groups_url` is a genuine unknown helper and still fires.
|
|
83
|
+
# The bare `<prefix>_path` (the namespace root) matches too.
|
|
84
|
+
def grape_match?(name)
|
|
85
|
+
return false if @grape_prefixes.empty?
|
|
86
|
+
return false unless name.end_with?("_path")
|
|
87
|
+
|
|
88
|
+
stem = name.delete_suffix("_path")
|
|
89
|
+
@grape_prefixes.any? { |prefix| stem == prefix || stem.start_with?("#{prefix}_") }
|
|
90
|
+
end
|
|
91
|
+
|
|
74
92
|
# `<resource>_<provider>_omniauth_(authorize|callback)_(path|url)` — when `<resource>` matches a
|
|
75
93
|
# declared `devise_for` resource the helper is dynamic-provider Devise OmniAuth. The provider
|
|
76
94
|
# segment is opaque to a static parser, so we accept any non-empty token between the resource and
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rigor/source/node_children"
|
|
4
|
+
|
|
3
5
|
require "prism"
|
|
4
6
|
require "rigor/source/literals"
|
|
5
7
|
|
|
@@ -56,9 +58,11 @@ module Rigor
|
|
|
56
58
|
# draw partial from `config/routes/name.rb`. Returns file contents
|
|
57
59
|
# or nil when the file is absent.
|
|
58
60
|
# @return [HelperTable]
|
|
59
|
-
def parse(contents, file_reader: nil, custom_helpers: [])
|
|
61
|
+
def parse(contents, file_reader: nil, custom_helpers: [], grape_prefixes: [])
|
|
60
62
|
parse_result = Prism.parse(contents)
|
|
61
|
-
|
|
63
|
+
unless parse_result.errors.empty?
|
|
64
|
+
return HelperTable.new([], custom_helpers: custom_helpers, grape_prefixes: grape_prefixes)
|
|
65
|
+
end
|
|
62
66
|
|
|
63
67
|
context = Context.new(file_reader: file_reader)
|
|
64
68
|
interpret(parse_result.value, context)
|
|
@@ -80,7 +84,8 @@ module Rigor
|
|
|
80
84
|
)
|
|
81
85
|
]
|
|
82
86
|
end
|
|
83
|
-
HelperTable.new(paired, custom_helpers: custom_helpers, devise_resources: context.devise_resources
|
|
87
|
+
HelperTable.new(paired, custom_helpers: custom_helpers, devise_resources: context.devise_resources,
|
|
88
|
+
grape_prefixes: grape_prefixes)
|
|
84
89
|
end
|
|
85
90
|
|
|
86
91
|
# For every registered alias rule `(from_str, to_str, arity_delta)`, find every existing entry whose
|
|
@@ -368,7 +373,7 @@ module Rigor
|
|
|
368
373
|
when Prism::CallNode
|
|
369
374
|
interpret_call(node, context)
|
|
370
375
|
else
|
|
371
|
-
node.
|
|
376
|
+
node.rigor_each_child { |child| interpret(child, context) }
|
|
372
377
|
end
|
|
373
378
|
end
|
|
374
379
|
|
|
@@ -523,7 +528,7 @@ module Rigor
|
|
|
523
528
|
return unless node.is_a?(Prism::Node)
|
|
524
529
|
|
|
525
530
|
yield node
|
|
526
|
-
node.
|
|
531
|
+
node.rigor_each_child { |child| walk_for_alias_pattern(child, &) }
|
|
527
532
|
end
|
|
528
533
|
|
|
529
534
|
def first_block_parameter_name(block_node)
|
|
@@ -542,7 +547,7 @@ module Rigor
|
|
|
542
547
|
body = node.block&.body
|
|
543
548
|
return if body.nil?
|
|
544
549
|
|
|
545
|
-
body.
|
|
550
|
+
body.rigor_each_child { |child| interpret(child, context) }
|
|
546
551
|
end
|
|
547
552
|
|
|
548
553
|
def handle_namespace(node, context)
|
|
@@ -622,7 +627,7 @@ module Rigor
|
|
|
622
627
|
return [] if body.nil?
|
|
623
628
|
|
|
624
629
|
skips = []
|
|
625
|
-
body.
|
|
630
|
+
body.rigor_each_child do |child|
|
|
626
631
|
next unless child.is_a?(Prism::CallNode) && child.name == :skip_controllers
|
|
627
632
|
next if child.receiver
|
|
628
633
|
|
|
@@ -745,7 +750,7 @@ module Rigor
|
|
|
745
750
|
body = context.concern_body(concern_name)
|
|
746
751
|
next if body.nil?
|
|
747
752
|
|
|
748
|
-
body.
|
|
753
|
+
body.rigor_each_child { |child| interpret(child, context) }
|
|
749
754
|
end
|
|
750
755
|
end
|
|
751
756
|
|
|
@@ -808,6 +813,12 @@ module Rigor
|
|
|
808
813
|
# String key in the trailing keyword/options hash (its value is the `controller#action`). Without
|
|
809
814
|
# this, the arity check underestimates because the placeholder count comes from a nil path.
|
|
810
815
|
path = string_argument(node, 0) || hashrocket_path_key(node)
|
|
816
|
+
# `get :activity` inside a `scope(as: :user)` — NOT a resources / member / collection block (that
|
|
817
|
+
# shape returned early above), so the symbol is both the path segment (`/activity`) and the action
|
|
818
|
+
# name. Rails composes `<scope_as>_activity_path`. Fall the symbol back into the path so the
|
|
819
|
+
# name-and-arity derivation below picks it up; without it the route registered nothing and every
|
|
820
|
+
# `user_activity_path` call read as `unknown-helper`.
|
|
821
|
+
path ||= symbol_action_path(node)
|
|
811
822
|
as_name = keyword_symbol(node, :as)
|
|
812
823
|
return if as_name.nil? && path.nil?
|
|
813
824
|
|
|
@@ -844,6 +855,14 @@ module Rigor
|
|
|
844
855
|
end
|
|
845
856
|
end
|
|
846
857
|
|
|
858
|
+
# `/activity` for a bare `get :activity` first-arg symbol (used as both path and action name), else
|
|
859
|
+
# nil. Only reached outside a resources / member / collection block — the symbol-shorthand there is
|
|
860
|
+
# already handled as a member/collection action upstream.
|
|
861
|
+
def symbol_action_path(node)
|
|
862
|
+
symbol = symbol_argument(node, 0)
|
|
863
|
+
symbol && "/#{symbol}"
|
|
864
|
+
end
|
|
865
|
+
|
|
847
866
|
# Builds the helper name for an explicit `get` / `post` / `match` route. Rails uses two distinct
|
|
848
867
|
# shapes:
|
|
849
868
|
#
|
|
@@ -879,12 +898,24 @@ module Rigor
|
|
|
879
898
|
|
|
880
899
|
first_arg = node.arguments&.arguments&.first
|
|
881
900
|
return true if first_arg.is_a?(Prism::SymbolNode)
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
901
|
+
return false unless first_arg.is_a?(Prism::StringNode)
|
|
902
|
+
|
|
903
|
+
path = first_arg.unescaped
|
|
904
|
+
# A dynamic segment (`get 'foo/:id'`) is a real path, not a plain action name — leave it to the
|
|
905
|
+
# generic explicit-route handler.
|
|
906
|
+
return false if path.include?(":")
|
|
907
|
+
|
|
908
|
+
# A bare `get '/'` (or `''`) inside a `collection do` block is the collection root, not an action
|
|
909
|
+
# shorthand — Rails names it after the enclosing resource, not `<empty>_<plural>_path`. An empty
|
|
910
|
+
# normalized action name means "not a shorthand"; fall through to the generic handler.
|
|
911
|
+
return false if path.delete_prefix("/").tr("/", "_").empty?
|
|
912
|
+
|
|
913
|
+
# A single-segment string is an action name in any resource context (`get 'report'` == `get
|
|
914
|
+
# :report`). A MULTI-segment static path (`get 'granular/new'`) is an action too, but only inside
|
|
915
|
+
# an explicit `member do` / `collection do` block, where Rails' `Mapper.normalize_name` maps the
|
|
916
|
+
# slashes to underscores (`granular_new_<scope>_<plural>_path`). Outside such a block (a bare
|
|
917
|
+
# `resources` scope) a multi-segment path is a nested custom route, so keep the generic handler.
|
|
918
|
+
!path.include?("/") || !context.innermost_action_block.nil?
|
|
888
919
|
end
|
|
889
920
|
|
|
890
921
|
# Generates the member / collection action helper.
|
|
@@ -893,8 +924,10 @@ module Rigor
|
|
|
893
924
|
# Collection: `<action>_<plural_helper_prefix>path`,
|
|
894
925
|
# arity = parent_segment_count - 1 (no `:id` segment; the collection URL is /<resource>/<action>).
|
|
895
926
|
def register_member_collection_action(node, context)
|
|
927
|
+
# A multi-segment string action (`get 'granular/new'` in a `collection do` block) is named by
|
|
928
|
+
# Rails via `normalize_name` — leading slash stripped, remaining slashes → underscores.
|
|
896
929
|
action_name = symbol_argument(node, 0)&.to_s ||
|
|
897
|
-
string_argument(node, 0).to_s
|
|
930
|
+
string_argument(node, 0).to_s.delete_prefix("/").tr("/", "_")
|
|
898
931
|
frame = context.innermost_action_block
|
|
899
932
|
# No `member do` / `collection do` wrapper but we're inside a resources block — Rails defaults a
|
|
900
933
|
# bare `<verb> :symbol` inside resources to a MEMBER action (e.g. `get :preview` →
|
|
@@ -5,6 +5,7 @@ require "rigor/plugin"
|
|
|
5
5
|
require_relative "rails_routes/helper_table"
|
|
6
6
|
require_relative "rails_routes/routes_parser"
|
|
7
7
|
require_relative "rails_routes/helper_discoverer"
|
|
8
|
+
require_relative "rails_routes/grape_api_discoverer"
|
|
8
9
|
require_relative "rails_routes/analyzer"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
@@ -34,6 +35,11 @@ module Rigor
|
|
|
34
35
|
# config:
|
|
35
36
|
# routes_file: config/routes.rb # default; optional
|
|
36
37
|
#
|
|
38
|
+
# A project mounting a Grape API keeps its `grape-path-helpers` helpers (`api_v4_groups_badges_path`)
|
|
39
|
+
# out of `unknown-helper`: those names come from grape's runtime route table, so {GrapeApiDiscoverer}
|
|
40
|
+
# recognises the namespace their `prefix` / `version` declarations open rather than enumerating names
|
|
41
|
+
# it cannot know.
|
|
42
|
+
#
|
|
37
43
|
# ## Limitations (v0.1.0)
|
|
38
44
|
#
|
|
39
45
|
# - `scope :path:` / `scope :module:` / `scope :as:` are not interpreted — helpers nested inside these
|
|
@@ -50,8 +56,10 @@ module Rigor
|
|
|
50
56
|
# ':project_id', as: :project)` — path read from the `:path` keyword, not only from the positional
|
|
51
57
|
# first arg; (c) detection of iterative `direct(name.sub(FROM, TO)) do ... end` alias-generation
|
|
52
58
|
# idiom — generates `<TO>_*` aliases for every registered `<FROM>_*` helper. GitLab uses this to
|
|
53
|
-
# shorten `namespace_project_*` → `project_*`.
|
|
54
|
-
|
|
59
|
+
# shorten `namespace_project_*` → `project_*`. Bumped 2026-07-10 — recognises the open helper
|
|
60
|
+
# namespace `grape-path-helpers` generates (`api_v4_*_path`), grounded in the project's own Grape
|
|
61
|
+
# `prefix` / `version` declarations (see {GrapeApiDiscoverer}).
|
|
62
|
+
version: "0.28.0",
|
|
55
63
|
description: "Validates Rails route-helper calls against `config/routes.rb`.",
|
|
56
64
|
config_schema: {
|
|
57
65
|
"routes_file" => { kind: :string, default: "config/routes.rb" },
|
|
@@ -63,7 +71,12 @@ module Rigor
|
|
|
63
71
|
# `app/serializers`, `app/presenters`, `app/decorators`, not only `app/helpers/`. Walking the
|
|
64
72
|
# whole tree is the honest answer to "does this `_path` / `_url` name exist anywhere in the
|
|
65
73
|
# project?"; the cost is a one-time Prism parse per file at startup, which is bounded.
|
|
66
|
-
"helper_paths" => { kind: :array, default: ["app"] }
|
|
74
|
+
"helper_paths" => { kind: :array, default: ["app"] },
|
|
75
|
+
# `grape_api_paths` — the directories scanned for Grape API classes, whose `prefix` / `version`
|
|
76
|
+
# declarations ground the open `api_v4_*_path` helper namespace `grape-path-helpers` generates
|
|
77
|
+
# (see {GrapeApiDiscoverer}). Defaults cover the two conventional homes; a project with no grape
|
|
78
|
+
# API declares no prefixes and nothing changes for it.
|
|
79
|
+
"grape_api_paths" => { kind: :array, default: ["lib/api", "app/api"] }
|
|
67
80
|
},
|
|
68
81
|
produces: [:helper_table]
|
|
69
82
|
)
|
|
@@ -73,11 +86,12 @@ module Rigor
|
|
|
73
86
|
# routes-file edits.
|
|
74
87
|
#
|
|
75
88
|
# Passes a `file_reader` lambda so the parser can follow `draw(:admin)` → `config/routes/admin.rb`
|
|
76
|
-
# partials. `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:`
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
89
|
+
# partials. `watch:` (ADR-60 WD3) covers every `.rb` under `helper_paths:` and `grape_api_paths:` so
|
|
90
|
+
# ADDING a helper file — or a Grape API whose `version` declaration opens a new namespace —
|
|
91
|
+
# invalidates the table. The producer's own in-block reads (the routes file + the partials it follows
|
|
92
|
+
# via `draw`) are captured post-compute, but a brand-new file the prior run never read would otherwise
|
|
93
|
+
# read as fresh.
|
|
94
|
+
producer :helper_table, watch: -> { (@helper_paths + @grape_api_paths).map { |dir| [dir, "**/*.rb"] } } do |_p|
|
|
81
95
|
routes_dir = "#{File.dirname(@routes_file)}/routes"
|
|
82
96
|
file_reader = lambda do |name|
|
|
83
97
|
io_boundary.read_file("#{routes_dir}/#{name}")
|
|
@@ -85,13 +99,14 @@ module Rigor
|
|
|
85
99
|
nil
|
|
86
100
|
end
|
|
87
101
|
contents = io_boundary.read_file(@routes_file)
|
|
88
|
-
custom_helpers
|
|
89
|
-
|
|
102
|
+
RoutesParser.parse(contents, file_reader: file_reader, custom_helpers: discover_custom_helpers,
|
|
103
|
+
grape_prefixes: discover_grape_prefixes)
|
|
90
104
|
end
|
|
91
105
|
|
|
92
106
|
def init(_services)
|
|
93
107
|
@routes_file = config.fetch("routes_file")
|
|
94
108
|
@helper_paths = Array(config.fetch("helper_paths")).map(&:to_s)
|
|
109
|
+
@grape_api_paths = Array(config.fetch("grape_api_paths")).map(&:to_s)
|
|
95
110
|
@helper_table = nil
|
|
96
111
|
@helper_table_built = false
|
|
97
112
|
@load_error = nil
|
|
@@ -112,8 +127,23 @@ module Rigor
|
|
|
112
127
|
HelperDiscoverer.discover(contents_per_path)
|
|
113
128
|
end
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
# Walks `grape_api_paths:` through the trusted `IoBoundary` and returns the helper-name prefixes the
|
|
131
|
+
# project's Grape API classes generate (`["api_v3", "api_v4"]`). Empty for a project with no grape API,
|
|
132
|
+
# which leaves `unknown-helper` behaviour exactly as it was.
|
|
133
|
+
def discover_grape_prefixes
|
|
134
|
+
contents_per_path = {}
|
|
135
|
+
each_ruby_file(@grape_api_paths) do |path|
|
|
136
|
+
contents_per_path[path] = io_boundary.read_file(path)
|
|
137
|
+
rescue Plugin::AccessDeniedError, Errno::ENOENT
|
|
138
|
+
next
|
|
139
|
+
end
|
|
140
|
+
GrapeApiDiscoverer.discover(contents_per_path)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def each_helper_file(&) = each_ruby_file(@helper_paths, &)
|
|
144
|
+
|
|
145
|
+
def each_ruby_file(dirs, &)
|
|
146
|
+
dirs.each do |dir|
|
|
117
147
|
absolute = File.expand_path(dir)
|
|
118
148
|
next unless File.directory?(absolute)
|
|
119
149
|
|