rigortype 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../typing_state"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "closure_escape_analyzer"
|
|
8
|
+
require_relative "element_read_widening"
|
|
9
|
+
require_relative "mutation_widening"
|
|
10
|
+
require_relative "receiver_alias"
|
|
11
|
+
require_relative "remover_holder_scan"
|
|
12
|
+
require_relative "set_mutation"
|
|
13
|
+
require_relative "string_mutation"
|
|
14
|
+
require_relative "unknown_store_widening"
|
|
15
|
+
require_relative "alias_mutation_floor/foreign_locals"
|
|
16
|
+
require_relative "alias_mutation_floor/memos"
|
|
17
|
+
require_relative "alias_mutation_floor/site_collector"
|
|
18
|
+
require_relative "alias_mutation_floor/reach"
|
|
19
|
+
|
|
20
|
+
module Rigor
|
|
21
|
+
module Inference
|
|
22
|
+
# Issue #2335 — a local's literal shape describes its object, and a mutation through another path to that object
|
|
23
|
+
# makes it stale: `y = x; y << "s"`, `a.each { |x| x << "s" }` on an Array holding `inner`, `@a = a; @a << "s"`,
|
|
24
|
+
# `h[:k] = a; h[:k] << "s"`, `Hash.new(a)[:k] << "s"`, `[acc].each { |a| poke(a) }`. The named-binding widening
|
|
25
|
+
# ({MutationWidening.widen_after_call}) and the callee floor only rebind the binding a receiver or argument names,
|
|
26
|
+
# so every other local holding the object kept its literal and a later read reported on correct code.
|
|
27
|
+
#
|
|
28
|
+
# One scan per frame body, in {RemoverHolderScan}'s walk ({SiteCollector}), answers:
|
|
29
|
+
#
|
|
30
|
+
# - the locals handed whole to another holder: an element of a container literal, an argument of a call that may
|
|
31
|
+
# keep it, an ivar, global, index or attribute write's value, a capture by a closure that may run later, a
|
|
32
|
+
# reflective receiver, a local bound to an ivar, global or class variable (`a = @q`); every local under
|
|
33
|
+
# `binding` / `eval`. A value counts only where it may evaluate to the local itself ({.each_may_be}: the local,
|
|
34
|
+
# a selection of it, a call that answers its receiver), so `n = a.size`, `b = a.dup` and `[a.first]` hand
|
|
35
|
+
# nothing on whole;
|
|
36
|
+
# - the locals one of whose elements is handed on (`[a.first]`), and the links between locals: a copy (`b = a`)
|
|
37
|
+
# and an element read (`x = a[i]`);
|
|
38
|
+
# - the *foreign* locals, whose object may have come from elsewhere ({ForeignLocals}).
|
|
39
|
+
#
|
|
40
|
+
# A local is *aliased* at a call once a site that hands it on may have run before the call's message (a site
|
|
41
|
+
# earlier in the body, or one inside a loop or block that also holds the call; a capture by a closure that may run
|
|
42
|
+
# later counts everywhere, and a call inside such a closure sees every site), or it is linked to such a local. A
|
|
43
|
+
# local reached only through an element link is a *part*, floored only when its binding can hold a mutable
|
|
44
|
+
# element ({.mutable_parts?}).
|
|
45
|
+
#
|
|
46
|
+
# A mutating call whose receiver may be another local's object ({.targets}: the locals linked to the receiver, and
|
|
47
|
+
# the aliased ones when the receiver may hold any object) gives each local it may reach the
|
|
48
|
+
# unknown store {UnknownStoreWidening.widen_store} gives a site whose stored values nothing typed: the same
|
|
49
|
+
# widening the named path applies, fed `Dynamic[top]` arguments, with its gradual arm. A callee that writes such
|
|
50
|
+
# an argument, and a call on one to a method that mutates its instance variables, floor them the same way. The
|
|
51
|
+
# bindings the receiver itself names are left to the named path, so its precise answer stands.
|
|
52
|
+
#
|
|
53
|
+
# A body where no local is handed on, and a receiver only ever written a new object that is not aliased, change
|
|
54
|
+
# nothing.
|
|
55
|
+
module AliasMutationFloor
|
|
56
|
+
# `held` maps each local handed to another holder to the least key of those sites ({.handed_by}), or is {ALL};
|
|
57
|
+
# `held_part` does the same for a local an element of which was handed on (`[a.first]`); `links` lists
|
|
58
|
+
# `[child, parent, key, levels]` for each write that binds a local to another local's object (`b = a`, 0 levels)
|
|
59
|
+
# or to an element of it (`x = a[i]`, 1; `x = a[i][j]`, 2; `c = [x]` links `x` as `c`'s element). `deferred`
|
|
60
|
+
# lists the `[start, end)` ranges of the closures that may run later. `memo` and `named_memo` keep each call's and
|
|
61
|
+
# each read's targets for the run: a loop or block body is evaluated more than once.
|
|
62
|
+
Facts = Data.define(:held, :held_part, :links, :foreign, :entry, :deferred, :memo, :named_memo)
|
|
63
|
+
# Depths ({.reach}): the mutated object is the local's object itself, or one of its elements.
|
|
64
|
+
FULL = 0
|
|
65
|
+
PART = 1
|
|
66
|
+
NO_KEYS = {}.freeze
|
|
67
|
+
NO_LINKS = [].freeze
|
|
68
|
+
# Calls whose result is their receiver (or, for `to_a` / `to_h` / `to_s`, may be on one core class), as
|
|
69
|
+
# {CalleeWriteCensus} reads them.
|
|
70
|
+
SELF_RETURNERS = %i[tap itself freeze to_a to_h to_s].to_set.freeze
|
|
71
|
+
|
|
72
|
+
ALL = :all
|
|
73
|
+
NUMBERED_PARAMETER = /\A_[1-9]\z/
|
|
74
|
+
SEND_NAMES = %i[send public_send __send__].to_set.freeze
|
|
75
|
+
ELEMENT_READS = %i[[] first last fetch dig].to_set.freeze
|
|
76
|
+
private_constant :FULL, :PART, :NO_KEYS, :NO_LINKS, :SELF_RETURNERS, :ALL, :NUMBERED_PARAMETER,
|
|
77
|
+
:SEND_NAMES, :ELEMENT_READS
|
|
78
|
+
private_constant :Memos
|
|
79
|
+
|
|
80
|
+
module_function
|
|
81
|
+
|
|
82
|
+
# `scope` after `call_node`, a shape mutator ({.candidate?}), ran on a receiver that may be another local's
|
|
83
|
+
# object. The block answers the locals the receiver itself names, whose own widening (the named path's)
|
|
84
|
+
# stands; it runs only once the call has targets.
|
|
85
|
+
def floor_after_call(call_node, scope)
|
|
86
|
+
return scope unless local_besides?(scope, nil) { |type| UnknownStoreWidening.callee_floorable?(type) }
|
|
87
|
+
|
|
88
|
+
facts = facts_for(scope)
|
|
89
|
+
return scope if facts.nil?
|
|
90
|
+
|
|
91
|
+
targets = call_targets(facts, call_node) { receiver_targets(call_node.receiver, facts, call_node) }
|
|
92
|
+
return scope if targets.empty?
|
|
93
|
+
|
|
94
|
+
whole = !targets.equal?(ALL)
|
|
95
|
+
floor_targets(scope, targets, yield) do |type, kind|
|
|
96
|
+
unknown_store(call_node, type, removed_keys: whole && kind == FULL)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Whether `call_node` is a shape mutator on an explicit receiver, the only call {.floor_after_call} floors after.
|
|
101
|
+
def candidate?(call_node)
|
|
102
|
+
!call_node.receiver.nil? && MutationWidening::SHAPE_MUTATORS.include?(call_node.name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# `scope` after `call_node` wrote the object the local `read` reads (a callee that writes its argument or mutates
|
|
106
|
+
# the ivars of an object holding it): every local that may be that object and holds a literal shape
|
|
107
|
+
# ({.literal_shape?}) takes one unknown store ({UnknownStoreWidening.callee_block_floor}). A method parameter the
|
|
108
|
+
# body never rebinds floors nothing here: the callee census reads every opaque hand-off as a write, and flooring
|
|
109
|
+
# each local a parameter was copied into at every such call cost precision across the corpus for no finding (a
|
|
110
|
+
# known limit of #2335).
|
|
111
|
+
def floor_after_written_local(scope, read, call_node)
|
|
112
|
+
name = read.name
|
|
113
|
+
return scope unless local_besides?(scope, name) { |type| literal_shape?(type) }
|
|
114
|
+
|
|
115
|
+
facts = facts_for(scope)
|
|
116
|
+
return scope if facts.nil?
|
|
117
|
+
return scope if facts.entry.include?(name)
|
|
118
|
+
|
|
119
|
+
targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
|
|
120
|
+
return scope if targets.empty?
|
|
121
|
+
|
|
122
|
+
floor_targets(scope, targets, name) { |type| callee_floor(type) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# `type` after `call_node`'s unknown store ({UnknownStoreWidening.widen_store}), memoised per call site for the
|
|
126
|
+
# run as {.callee_floor} is.
|
|
127
|
+
def unknown_store(call_node, type, removed_keys:)
|
|
128
|
+
Memos.unknown_store(call_node, removed_keys, type) do
|
|
129
|
+
method_name, arg_types = UnknownStoreWidening.unknown_store(call_node)
|
|
130
|
+
(removed_keys && removed_keys_shape(type, method_name)) ||
|
|
131
|
+
UnknownStoreWidening.widen_store(type, method_name, arg_types)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A `HashShape` under a remover (`delete`, `clear`, `reject!`, ...) through another holder: the same shape with
|
|
136
|
+
# every key optional. Nothing is stored, so every value stands, and a read of a key the remover may have taken
|
|
137
|
+
# still answers its `nil` (`g = h; g.delete(k); h[k].upcase` raises in Ruby), which the nominal `Hash` the unknown
|
|
138
|
+
# store widens to would not. Only for a target that may be the mutated object itself, and not under `binding` /
|
|
139
|
+
# `eval`: a remover on an element (`x = h[:k]; x.delete(:a)`) must not mark `h`'s own keys optional and leave the
|
|
140
|
+
# element's removed key required. Nil for anything else.
|
|
141
|
+
def removed_keys_shape(type, method_name)
|
|
142
|
+
return nil unless type.is_a?(Type::HashShape) && UnknownStoreWidening::VALUE_PRESERVING.include?(method_name)
|
|
143
|
+
|
|
144
|
+
Type::HashShape.new(type.pairs, required_keys: [], optional_keys: type.pairs.keys,
|
|
145
|
+
read_only_keys: type.read_only_keys, extra_keys: type.extra_keys)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# {UnknownStoreWidening.callee_block_floor} of a literal shape, nil for anything else, memoised for the run: the
|
|
149
|
+
# same shape is floored again on each pass over a loop or block body and by each callee channel.
|
|
150
|
+
def callee_floor(type)
|
|
151
|
+
return nil unless literal_shape?(type)
|
|
152
|
+
|
|
153
|
+
Memos.callee_floor(type) { UnknownStoreWidening.callee_block_floor(type) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Whether {.floor_after_written_local} may floor anything for the local `read` reads at `call_node`: a target
|
|
157
|
+
# bound to a literal shape.
|
|
158
|
+
def reaches_other_locals?(scope, read, call_node)
|
|
159
|
+
name = read.name
|
|
160
|
+
return false unless local_besides?(scope, name) { |type| literal_shape?(type) }
|
|
161
|
+
|
|
162
|
+
facts = facts_for(scope)
|
|
163
|
+
return false if facts.nil? || facts.entry.include?(name)
|
|
164
|
+
|
|
165
|
+
targets = read_targets(facts, read) { targets(name, facts, aliased_at(facts, call_node), call_node) }
|
|
166
|
+
return local_besides?(scope, name) { |type| literal_shape?(type) } if targets.equal?(ALL)
|
|
167
|
+
|
|
168
|
+
targets.any? do |target, kind|
|
|
169
|
+
type = scope.local(target)
|
|
170
|
+
target != name && literal_shape?(type) && (kind == FULL || mutable_parts?(type))
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Whether a call that may mutate a holder's object through `self` ({.floor_after_holder_call}) reaches an aliased
|
|
175
|
+
# local holding a literal shape.
|
|
176
|
+
def holder_call_reaches?(scope, call_node)
|
|
177
|
+
return false unless local_besides?(scope, nil) { |type| literal_shape?(type) }
|
|
178
|
+
|
|
179
|
+
facts = facts_for(scope)
|
|
180
|
+
return false if facts.nil?
|
|
181
|
+
|
|
182
|
+
targets = holder_targets(facts, call_node)
|
|
183
|
+
return true if targets.equal?(ALL)
|
|
184
|
+
|
|
185
|
+
targets.any? do |target, kind|
|
|
186
|
+
type = scope.local(target)
|
|
187
|
+
literal_shape?(type) && (kind == FULL || mutable_parts?(type))
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `scope` after a call on `self` whose method mutates one of its ivars: every aliased local holding a literal
|
|
192
|
+
# shape takes one unknown store, as {.floor_after_written_local} gives it.
|
|
193
|
+
def floor_after_holder_call(scope, call_node)
|
|
194
|
+
facts = facts_for(scope)
|
|
195
|
+
return scope if facts.nil?
|
|
196
|
+
|
|
197
|
+
floor_targets(scope, holder_targets(facts, call_node), nil) { |type| callee_floor(type) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def holder_targets(facts, call_node)
|
|
201
|
+
call_targets(facts, call_node) { targets(nil, facts, aliased_at(facts, call_node), call_node) }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A literal shape a store can make stale: a `Tuple`, a `HashShape`, a literal String or Set, or an empty-witness
|
|
205
|
+
# refinement, alone or in a union. The callee paths floor only these: a nominal `Array[String]` is a claim the
|
|
206
|
+
# named path's unknown store leaves as it is too.
|
|
207
|
+
def literal_shape?(type)
|
|
208
|
+
case type
|
|
209
|
+
when Type::Tuple, Type::HashShape then true
|
|
210
|
+
when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
|
|
211
|
+
when Type::Difference then type.removes_empty_witness?
|
|
212
|
+
when Type::Union then type.members.any? { |member| literal_shape?(member) }
|
|
213
|
+
else false
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Whether the body of `def_node` runs a shape mutator on one of its own instance variables or an element of one
|
|
218
|
+
# (`@items << v`, `@h[k] = v`, `@rows[0] << v`): a call to it on an object that holds another local's object
|
|
219
|
+
# (`box = Box.new(a)`, whose `initialize` keeps `@items = a`) may mutate that object. A nested def is not entered.
|
|
220
|
+
def mutates_ivar?(def_node)
|
|
221
|
+
return false if def_node.nil?
|
|
222
|
+
|
|
223
|
+
Memos.ivar_mutator(def_node) { ivar_mutation?(def_node.body) }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def ivar_mutation?(node)
|
|
227
|
+
return false if node.nil? || node.is_a?(Prism::DefNode)
|
|
228
|
+
return true if node.is_a?(Prism::CallNode) && MutationWidening::SHAPE_MUTATORS.include?(node.name) &&
|
|
229
|
+
ivar_rooted?(node.receiver)
|
|
230
|
+
|
|
231
|
+
node.rigor_each_child { |child| return true if ivar_mutation?(child) }
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def ivar_rooted?(receiver)
|
|
236
|
+
receiver = RemoverHolderScan.unparenthesised(receiver)
|
|
237
|
+
receiver = receiver.receiver while receiver.is_a?(Prism::CallNode) && ELEMENT_READS.include?(receiver.name)
|
|
238
|
+
receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# A `send` / `public_send` / `__send__` with a literal Symbol or String name as the call it makes (`a.send(:push,
|
|
242
|
+
# x)` and `a.send("push", x)` are `a.push(x)`), or nil. A name that is no valid string names no method.
|
|
243
|
+
def literal_send_target(call_node)
|
|
244
|
+
return nil unless SEND_NAMES.include?(call_node.name) && call_node.block.nil?
|
|
245
|
+
|
|
246
|
+
arguments = call_node.arguments&.arguments
|
|
247
|
+
target = arguments&.first
|
|
248
|
+
return nil unless target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode)
|
|
249
|
+
return nil unless target.unescaped.valid_encoding?
|
|
250
|
+
|
|
251
|
+
rest = arguments.drop(1)
|
|
252
|
+
call_node.copy(name: target.unescaped.to_sym,
|
|
253
|
+
arguments: rest.empty? ? nil : call_node.arguments.copy(arguments: rest))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def facts_for(scope)
|
|
257
|
+
frame = scope.match_frame
|
|
258
|
+
body = frame&.body
|
|
259
|
+
return nil if body.nil?
|
|
260
|
+
|
|
261
|
+
facts = Memos.facts(body) { scan(body, frame.parameters) }
|
|
262
|
+
facts.held.empty? && facts.held_part.empty? && facts.links.empty? ? nil : facts
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# One walk ({SiteCollector}) answers the facts. A body that calls `binding` or `eval` stops it early: every local
|
|
266
|
+
# is aliased, and the foreign locals are read in a walk of their own.
|
|
267
|
+
def scan(body, parameters)
|
|
268
|
+
collector = SiteCollector.new
|
|
269
|
+
result = RemoverHolderScan.unsafe_names(body, parameters, collector)
|
|
270
|
+
if result.equal?(ALL)
|
|
271
|
+
foreign, entry = ForeignLocals.collect(body, parameters)
|
|
272
|
+
return Facts.new(held: ALL, held_part: NO_KEYS, links: NO_LINKS, foreign:, entry:, deferred: NO_LINKS,
|
|
273
|
+
memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
foreign, entry = ForeignLocals.finish(parameters, collector.foreign, collector.written)
|
|
277
|
+
Facts.new(held: collector.held.freeze, held_part: collector.held_part.freeze, links: collector.links.freeze,
|
|
278
|
+
foreign:, entry:, deferred: collector.deferred.freeze,
|
|
279
|
+
memo: {}.compare_by_identity, named_memo: {}.compare_by_identity)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A site's key ({SiteCollector}) is the start of the outermost loop or block that holds it (it may run again after
|
|
283
|
+
# any later point inside that loop), else where it has handed its locals on: its end (the receiver of a call has
|
|
284
|
+
# handed its locals on before the call's message runs, while the call's own arguments are handed on by the call
|
|
285
|
+
# itself), or its block's start. A local's key is its least site's; a capture by a closure that may run later is
|
|
286
|
+
# {ALWAYS}.
|
|
287
|
+
# Where a site has handed its locals on: a call's block may receive them as soon as it starts.
|
|
288
|
+
def handed_by(node)
|
|
289
|
+
block = node.block if node.is_a?(Prism::CallNode)
|
|
290
|
+
block.is_a?(Prism::BlockNode) ? block.location.start_offset : node.location.end_offset
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The targets of `call_node` through its receiver, computed once per call site.
|
|
294
|
+
def call_targets(facts, call_node)
|
|
295
|
+
facts.memo.fetch(call_node) { facts.memo[call_node] = yield }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# The targets of a write through the local `read` reads (a written argument, a receiver), computed once per read.
|
|
299
|
+
# The read is the call's own operand, so its call's offset is fixed with it.
|
|
300
|
+
def read_targets(facts, read)
|
|
301
|
+
facts.named_memo.fetch(read) { facts.named_memo[read] = yield }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Whether `scope` binds a local other than `except` (a name, a list of names, or nil) whose type the block
|
|
305
|
+
# accepts: the cheap gate every trigger asks before the body scan, since a floor changes only such a binding.
|
|
306
|
+
def local_besides?(scope, except)
|
|
307
|
+
scope.locals.each { |name, type| return true if !excepted?(except, name) && yield(type) }
|
|
308
|
+
false
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def excepted?(except, name)
|
|
312
|
+
except.is_a?(Symbol) ? except == name : except&.include?(name)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def each_target(scope, targets, &)
|
|
316
|
+
return scope.locals.each_key { |name| yield name, FULL } if targets.equal?(ALL)
|
|
317
|
+
|
|
318
|
+
targets.each(&)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Each target but `except` takes what the block answers for its binding and the depth it was reached at. A target
|
|
322
|
+
# reached only below its own object (a depth past {FULL}) is floored only when its binding can hold a mutable
|
|
323
|
+
# element.
|
|
324
|
+
def floor_targets(scope, targets, except)
|
|
325
|
+
acc = scope
|
|
326
|
+
each_target(scope, targets) do |name, kind|
|
|
327
|
+
next if excepted?(except, name)
|
|
328
|
+
|
|
329
|
+
current = acc.local(name)
|
|
330
|
+
next if current.nil? || (kind > FULL && !mutable_parts?(current))
|
|
331
|
+
|
|
332
|
+
floored = yield(current, kind)
|
|
333
|
+
acc = acc.with_mutated_local(name, floored) unless floored.nil? || floored == current
|
|
334
|
+
end
|
|
335
|
+
acc
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -62,11 +62,29 @@ module Rigor
|
|
|
62
62
|
receiver_name = block_form_receiver(node)
|
|
63
63
|
return nil if receiver_name.nil?
|
|
64
64
|
|
|
65
|
+
named = struct_class_name(node) if receiver_name == :Struct
|
|
66
|
+
return named if named
|
|
67
|
+
|
|
65
68
|
location = node.location
|
|
66
69
|
key = [source_path, location.start_line, location.start_column].compact.join(":")
|
|
67
70
|
Type::AnonymousClassName.build(receiver_name, key)
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
# Issue #2968 — `Struct.new("Named", :a)` names its class `Struct::Named`: a leading String argument that spells a
|
|
74
|
+
# constant (Ruby raises `NameError` for any other) is the class's name under `Struct`, so a block body is that
|
|
75
|
+
# class's and a reopening `class Struct::Named` in another file meets the same table entry. Nil for any other
|
|
76
|
+
# first argument. `node` is a `Struct.new` call.
|
|
77
|
+
def struct_class_name(node)
|
|
78
|
+
first = node.arguments&.arguments&.first
|
|
79
|
+
return nil unless first.is_a?(Prism::StringNode)
|
|
80
|
+
|
|
81
|
+
name = first.unescaped
|
|
82
|
+
"Struct::#{name}" if name.match?(STRUCT_CLASS_NAME)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
STRUCT_CLASS_NAME = /\A[A-Z][A-Za-z0-9_]*\z/
|
|
86
|
+
private_constant :STRUCT_CLASS_NAME
|
|
87
|
+
|
|
70
88
|
# The bare-constant receiver's name (`Class`, `::Class`), or nil for every other receiver shape.
|
|
71
89
|
def literal_constant_name(node)
|
|
72
90
|
case node
|
|
@@ -194,7 +194,7 @@ module Rigor
|
|
|
194
194
|
softened = []
|
|
195
195
|
joined = Array.new(tables.map(&:size).max) do |i|
|
|
196
196
|
column = tables.map { |table| table[i] }
|
|
197
|
-
next Type::Combinator.untyped if column.any? { |t| t.nil? ||
|
|
197
|
+
next Type::Combinator.untyped if column.any? { |t| t.nil? || Type::Combinator.untyped?(t) }
|
|
198
198
|
|
|
199
199
|
firm = column.reject { |t| nil_literal?(t) }
|
|
200
200
|
next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "block_call_timing"
|
|
6
6
|
require_relative "closure_escape_analyzer"
|
|
7
|
+
require_relative "../memo_audit"
|
|
7
8
|
require_relative "project_method_ownership"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
@@ -50,9 +51,15 @@ module Rigor
|
|
|
50
51
|
# one, asks the same callee.
|
|
51
52
|
def may_repeat?(def_node, scope)
|
|
52
53
|
memo = ProjectMethodOwnership.memo(scope)[:block_runs]
|
|
53
|
-
|
|
54
|
+
if memo.key?(def_node)
|
|
55
|
+
return MemoAudit.check("block runs", memo[def_node], def_node) do
|
|
56
|
+
runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
|
|
57
|
+
end
|
|
58
|
+
end
|
|
54
59
|
|
|
55
|
-
memo[def_node] =
|
|
60
|
+
memo[def_node] = MemoAudit.key_audit("block runs") do
|
|
61
|
+
runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
|
|
62
|
+
end
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
def runs(node, block_name)
|
|
@@ -25,11 +25,11 @@ module Rigor
|
|
|
25
25
|
# Methods that yield (block-dependent) — classifier
|
|
26
26
|
# may mark them leaf when the block call is gated:
|
|
27
27
|
:each, :each_with_index, :each_index, :each_slice,
|
|
28
|
-
:each_cons, :each_with_object,
|
|
28
|
+
:each_cons, :each_with_object, :reverse_each,
|
|
29
29
|
# Identity/comparison methods that take a block too
|
|
30
30
|
:max, :min, :max_by, :min_by, :minmax, :minmax_by,
|
|
31
31
|
:sort_by, :group_by, :partition, :all?, :any?, :none?,
|
|
32
|
-
:one?, :find, :detect, :find_all, :find_index,
|
|
32
|
+
:one?, :find, :detect, :rfind, :find_all, :find_index, :index, :rindex,
|
|
33
33
|
:reduce, :inject, :flat_map, :collect_concat,
|
|
34
34
|
:zip, :product, :combination, :permutation,
|
|
35
35
|
:chunk_while, :slice_when, :tally
|
|
@@ -109,7 +109,7 @@ module Rigor
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
by_name[name] = compute_successors(def_node, name, owner, edges)
|
|
112
|
+
by_name[name] = MemoAudit.key_audit("successor") { compute_successors(def_node, name, owner, edges) }
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def compute_successors(def_node, name, owner, edges)
|
|
@@ -272,6 +272,8 @@ module Rigor
|
|
|
272
272
|
|
|
273
273
|
def ivar_name?(name) = variable_kind(name) == :ivar
|
|
274
274
|
|
|
275
|
+
def local_name?(name) = variable_kind(name) == :local
|
|
276
|
+
|
|
275
277
|
# Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
|
|
276
278
|
# `@`, and a local with none of them.
|
|
277
279
|
def variable_kind(name)
|