rigortype 0.4.0 → 0.4.2

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.
Files changed (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -121,7 +121,7 @@ module Rigor
121
121
  # syntax: this is the argument a shell just tab-completed, and `rigor effects app/models` meaning
122
122
  # "that directory" is the only reading a reader would guess.
123
123
  def self.normalize_scope(scope)
124
- Array(scope).map { |path| File.expand_path(path.to_s.chomp("/")) }.freeze
124
+ Array(scope).map { |path| File.absolute_path(path.to_s.chomp("/")) }.freeze
125
125
  end
126
126
  private_class_method :normalize_scope
127
127
 
@@ -130,7 +130,7 @@ module Rigor
130
130
  return false if paths.nil? || paths.empty?
131
131
 
132
132
  paths.any? do |path|
133
- absolute = File.expand_path(path)
133
+ absolute = File.absolute_path(path)
134
134
  roots.any? { |root| absolute == root || absolute.start_with?("#{root}/") }
135
135
  end
136
136
  end
@@ -69,7 +69,12 @@ module Rigor
69
69
  def execute(file:, options:)
70
70
  configuration = Configuration.load(options.fetch(:config))
71
71
  source = File.read(file, encoding: Encoding::UTF_8)
72
+ # Prism parses the ORIGINAL bytes, so a `# -*- coding: -*-` file is read in its own encoding. Only the text the
73
+ # renderer sees is scrubbed: the colouriser and line splitter raise on a byte invalid in UTF-8 (a comment,
74
+ # `__END__` data, a non-UTF-8 file).
75
+ # Scrubbing is byte-for-byte (`?` per bad byte), so Prism's positions still address it.
72
76
  parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
77
+ source = source.scrub { |bytes| "?" * bytes.bytesize } unless source.valid_encoding?
73
78
  return 1 if parse_errors?(parse_result, file)
74
79
 
75
80
  events = record_events(parse_result.value, file, configuration)
@@ -87,7 +87,7 @@ module Rigor
87
87
  # file is parsed and scope-indexed once, however many positions land in it.
88
88
  def execute(targets:, options:, buffer: nil)
89
89
  configuration = Configuration.load(options.fetch(:config))
90
- environment = project_environment(targets.map(&:file).uniq, configuration)
90
+ environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
91
91
  base_scope = Scope.empty(environment: environment)
92
92
 
93
93
  results_by_target = Array.new(targets.length)
@@ -218,8 +218,10 @@ module Rigor
218
218
  # closes). The probed file is threaded as the synthesizer's `source_files:`. Project-RBS auto-detection
219
219
  # roots at CWD today; future work will walk parent directories to find the enclosing `Gemfile`/`*.gemspec`
220
220
  # so probes against files outside the current process's CWD still see the right `sig/` tree.
221
- def project_environment(files, configuration)
222
- ProjectEnvironment.build(configuration: configuration, source_files: files)
221
+ # Issue #1700 — an editor buffer is scanned for required features in place of the file it stands for.
222
+ def project_environment(files, configuration, buffer = nil)
223
+ ProjectEnvironment.build(configuration: configuration, source_files: files,
224
+ feature_sources: ProjectEnvironment.buffer_sources(buffer))
223
225
  end
224
226
 
225
227
  def file_exists?(file)
@@ -51,7 +51,7 @@ module Rigor
51
51
  paths = @argv.empty? ? configuration.paths : @argv
52
52
  scan_cache = Analysis::Reachability::ScanCache.open(configuration.cache_path,
53
53
  target_ruby: configuration.target_ruby)
54
- declarations, references, dynamic_uses = scan(paths, configuration, scan_cache)
54
+ declarations, references, dynamic_uses, shadows = scan(paths, configuration, scan_cache)
55
55
  references.concat(signature_references(configuration))
56
56
  dynamic_uses.concat(template_mentions(declarations, scan_cache))
57
57
 
@@ -59,7 +59,8 @@ module Rigor
59
59
  cache_store: cache_store(configuration))
60
60
  references.concat(plugin_references(contribution.references))
61
61
  graph = build_graph(configuration, options, contribution,
62
- declarations: declarations, references: references, dynamic_uses: dynamic_uses)
62
+ declarations: declarations, references: references, dynamic_uses: dynamic_uses,
63
+ shadows: shadows)
63
64
  emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
64
65
  scan_cache.save
65
66
  0
@@ -67,9 +68,9 @@ module Rigor
67
68
 
68
69
  private
69
70
 
70
- def build_graph(configuration, options, contribution, declarations:, references:, dynamic_uses:)
71
+ def build_graph(configuration, options, contribution, declarations:, references:, dynamic_uses:, shadows:)
71
72
  Analysis::Reachability::Graph.new(
72
- declarations: declarations, references: references, dynamic_uses: dynamic_uses,
73
+ declarations: declarations, references: references, dynamic_uses: dynamic_uses, shadows: shadows,
73
74
  root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
74
75
  foreign: foreign_predicate(configuration)
75
76
  )
@@ -106,22 +107,25 @@ module Rigor
106
107
  end
107
108
 
108
109
  # Declarations come from the analysed paths; references additionally from the wider corpus (WD7).
110
+ # A wider-corpus file's own declarations come back as shadows: never nodes, only what that file's own
111
+ # references resolve to first (#1732).
109
112
  # Every file is consulted every run — an unchanged one contributes its cached scan, so the
110
113
  # whole-project completeness the `--incremental` refusal protects is unaffected.
111
114
  def scan(paths, configuration, cache)
112
115
  declaration_files = Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns).to_set
113
116
  declarations = []
117
+ shadows = []
114
118
  references = []
115
119
  dynamic_uses = []
116
120
  (declaration_files + reference_files(paths, configuration)).sort.each do |file|
117
121
  result = cache.serve(:scan, file) { read_and_scan(file, configuration) }
118
122
  next if result.nil?
119
123
 
120
- declarations.concat(result.declarations) if declaration_files.include?(file)
124
+ (declaration_files.include?(file) ? declarations : shadows).concat(result.declarations)
121
125
  references.concat(result.references)
122
126
  dynamic_uses.concat(result.dynamic_uses)
123
127
  end
124
- [declarations, references, dynamic_uses]
128
+ [declarations, references, dynamic_uses, shadows]
125
129
  end
126
130
 
127
131
  # The reference corpus is the PROJECT, not the analysed paths. A constant declared in `lib/` is commonly
@@ -131,13 +135,14 @@ module Rigor
131
135
  # ADR-102 WD2), which is exactly why the two corpora are separated rather than both widened.
132
136
  def reference_files(_paths, configuration)
133
137
  relative = Analysis::Reachability::ProjectFiles.own(Dir.glob(REFERENCE_GLOB, base: Dir.pwd), Dir.pwd)
134
- absolute = relative.map { |rel| File.expand_path(rel) }
138
+ absolute = relative.map { |rel| File.absolute_path(rel) }
135
139
  Analysis::PathExpansion.reject_excluded(absolute, configuration.exclude_patterns).to_set
136
140
  end
137
141
 
138
142
  def read_and_scan(file, configuration)
139
143
  Analysis::Reachability::Scan.call(path: file, source: File.read(file),
140
- target_ruby: configuration.target_ruby)
144
+ target_ruby: configuration.target_ruby,
145
+ root: File.absolute_path(Dir.pwd))
141
146
  rescue SystemCallError
142
147
  nil
143
148
  end
@@ -177,7 +182,7 @@ module Rigor
177
182
  return [] if names.empty?
178
183
 
179
184
  Analysis::Reachability::ProjectFiles.own(Dir.glob(TEMPLATE_GLOB, base: Dir.pwd), Dir.pwd).flat_map do |rel|
180
- absolute = File.expand_path(rel)
185
+ absolute = File.absolute_path(rel)
181
186
  haystack = cache.serve(:runs, absolute) { constant_bearing_text(File.read(absolute).scrub) }
182
187
  names.filter_map do |fqn|
183
188
  next unless haystack.include?(fqn)
@@ -208,7 +213,7 @@ module Rigor
208
213
  return [] if globs.empty?
209
214
 
210
215
  matcher = Analysis::Reachability::ProjectFiles.method(:entry_point_match?)
211
- cwd = "#{File.expand_path(Dir.pwd)}/"
216
+ cwd = "#{File.absolute_path(Dir.pwd)}/"
212
217
  declarations.filter_map do |d|
213
218
  relative = d.path.delete_prefix(cwd)
214
219
  d.fqn if globs.any? { |g| matcher.call(g, d.path) || matcher.call(g, relative) }
@@ -235,6 +240,7 @@ module Rigor
235
240
  def foreign_predicate(configuration)
236
241
  env = Environment.for_project(libraries: configuration.libraries, signature_paths: [],
237
242
  cache_store: cache_store(configuration),
243
+ required_features: ProjectEnvironment.required_features(configuration),
238
244
  **ProjectEnvironment.dependency_discovery_options(configuration))
239
245
  ->(fqn) { !env.singleton_for_name(fqn).nil? }
240
246
  rescue StandardError
data/lib/rigor/cli.rb CHANGED
@@ -281,6 +281,10 @@ module Rigor
281
281
  # tempfile, uri, logger, date, prism, rbs). Adding a
282
282
  # `sig/<gem>.rbs` file under `sig/` is the simplest way
283
283
  # to extend type coverage today.
284
+ #
285
+ # `target_ruby` is left commented: the default only picks the syntax Rigor parses. Set it to the lowest
286
+ # Ruby your project runs on, and Rigor also reports what that Ruby deprecates.
287
+ # target_ruby: #{Configuration::DEFAULTS.fetch('target_ruby').dump}
284
288
  #{YAML.dump(init_settings(path)).sub(/\A---\n/, '')}
285
289
  YAML
286
290
  end
@@ -288,13 +292,17 @@ module Rigor
288
292
  # The settings `rigor init` writes: the defaults, with `test_paths:` spelled out as the test roots found now, so
289
293
  # the project's config says where its tests are rather than leaving it to auto-detection. The roots are written
290
294
  # relative to the config file's directory, which is what {Configuration.load} resolves them against.
295
+ #
296
+ # `target_ruby` is left out (the template writes it commented): a written key is a statement about the runtime
297
+ # (#1692), and the starter must not make one the user never chose.
291
298
  def init_settings(path)
299
+ settings = Configuration::DEFAULTS.except("target_ruby")
292
300
  test_paths = Configuration.new.resolved_test_paths
293
- return Configuration::DEFAULTS if test_paths.empty?
301
+ return settings if test_paths.empty?
294
302
 
295
- config_dir = Pathname(File.expand_path(File.dirname(path)))
303
+ config_dir = Pathname(File.absolute_path(File.dirname(path)))
296
304
  relative = test_paths.map { |root| Pathname(root).relative_path_from(config_dir).to_s }
297
- Configuration::DEFAULTS.merge("test_paths" => relative)
305
+ settings.merge("test_paths" => relative)
298
306
  end
299
307
 
300
308
  def run_annotate
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "did_you_mean"
4
4
 
5
+ require_relative "config_path"
5
6
  require_relative "signature_path_audit"
6
7
  # ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
7
8
  # `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
@@ -199,14 +200,14 @@ module Rigor
199
200
  end
200
201
 
201
202
  def self.add_missing_dir(warnings, path, project_root, kind, key)
202
- return if path.nil? || File.directory?(File.expand_path(path, project_root))
203
+ return if path.nil? || File.directory?(ConfigPath.absolute(path, project_root))
203
204
 
204
205
  warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} is not a directory",
205
206
  fields: { "path" => path })
206
207
  end
207
208
 
208
209
  def self.add_missing_file(warnings, path, project_root, kind, key)
209
- return if path.nil? || File.file?(File.expand_path(path, project_root))
210
+ return if path.nil? || File.file?(ConfigPath.absolute(path, project_root))
210
211
 
211
212
  warnings << Warning.new(kind: kind, message: "#{key}: #{path.inspect} does not exist", fields: { "path" => path })
212
213
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Resolves a path-valued setting that a person wrote into `.rigor.yml`, or that Bundler wrote into its
5
+ # own config (`BUNDLE_PATH: "~/gems"`), to an absolute path.
6
+ #
7
+ # A leading `~/` and a bare `~` expand to the invoking user's home directory — exactly what
8
+ # `File.expand_path` does for those two spellings, and what Bundler itself does for `BUNDLE_PATH`. Every
9
+ # other spelling goes through `File.absolute_path`, so `~drafts/x` stays a directory named `~drafts`
10
+ # instead of `File.expand_path`'s home-of-user-`drafts` reading, which raises `ArgumentError` for a user
11
+ # that does not exist (#1510). It never raises on a `~`.
12
+ #
13
+ # Paths the analysis is *given to analyse* (CLI path arguments, template units, the runner's file sets)
14
+ # do not use this: the shell has already expanded a `~` there, so a surviving one is a directory name.
15
+ module ConfigPath
16
+ HOME_PREFIX = %r{\A~(?=/|\z)}
17
+
18
+ # `base_dir` anchors a relative result and defaults to the working directory.
19
+ def self.absolute(path, base_dir = nil)
20
+ text = path.to_s
21
+ return File.absolute_path(text, base_dir) unless text.match?(HOME_PREFIX)
22
+
23
+ home = Dir.home
24
+ File.absolute_path(text.sub(HOME_PREFIX, home), base_dir)
25
+ rescue ArgumentError
26
+ File.absolute_path(text, base_dir)
27
+ end
28
+ end
29
+ end
@@ -38,6 +38,10 @@ module Rigor
38
38
  "call.argument-type-mismatch" => :warning,
39
39
  "call.possible-nil-receiver" => :warning,
40
40
  "call.raise-non-exception" => :warning,
41
+ # Issue #1692 — the code runs on the stated Ruby (it warns, and breaks only at the removal version), so the
42
+ # rule takes the slot of the rules that report working-but-wrong code: `flow.unreachable-branch`'s
43
+ # info / warning / error. It runs only under an explicit `target_ruby` of 4.1 or later.
44
+ "call.deprecated-ruby2-keywords" => :info,
41
45
  "flow.always-raises" => :warning,
42
46
  "flow.unreachable-branch" => :info,
43
47
  "flow.dead-assignment" => :info,
@@ -100,6 +104,7 @@ module Rigor
100
104
  "call.argument-type-mismatch" => :error,
101
105
  "call.possible-nil-receiver" => :error,
102
106
  "call.raise-non-exception" => :error,
107
+ "call.deprecated-ruby2-keywords" => :warning,
103
108
  "flow.always-raises" => :error,
104
109
  "flow.unreachable-branch" => :warning,
105
110
  "flow.dead-assignment" => :warning,
@@ -141,6 +146,7 @@ module Rigor
141
146
  "call.argument-type-mismatch" => :error,
142
147
  "call.possible-nil-receiver" => :error,
143
148
  "call.raise-non-exception" => :error,
149
+ "call.deprecated-ruby2-keywords" => :error,
144
150
  "flow.always-raises" => :error,
145
151
  "flow.unreachable-branch" => :error,
146
152
  "flow.dead-assignment" => :error,
@@ -210,7 +216,12 @@ module Rigor
210
216
  profile_table.fetch(rule, authored_severity)
211
217
  end
212
218
 
219
+ # A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
220
+ # diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
221
+ # so the split below is skipped for them rather than allocating a name that nothing can match.
213
222
  def family_override(rule, overrides)
223
+ return nil unless overrides.any? { |key, _| !key.to_s.include?(".") }
224
+
214
225
  family = rule.split(".").first
215
226
  return nil if family.nil?
216
227
 
@@ -6,6 +6,7 @@ require_relative "bleeding_edge"
6
6
  require_relative "ci_detector"
7
7
  require_relative "configuration/dependencies"
8
8
  require_relative "configuration/severity_profile"
9
+ require_relative "config_path"
9
10
  require_relative "configuration_error"
10
11
  require_relative "effects/entry_points"
11
12
  require_relative "effects/label"
@@ -282,6 +283,23 @@ module Rigor
282
283
  :effects_snapshot_path, :effects_snapshot_reach, :effects_snapshot_gate, :effects_tolerated,
283
284
  :effects_labels, :effects_attribution, :effects_envelopes
284
285
 
286
+ # ADR-47 WD5 amendment (#1692) — whether `.rigor.yml` (or its `includes:` chain) set `target_ruby:` itself.
287
+ # The default `"4.0"` is a parse version, not something the user said about the Ruby the project runs on.
288
+ def target_ruby_explicit?
289
+ @target_ruby_explicit
290
+ end
291
+
292
+ # ADR-47 WD5 amendment (#1692) — `target_ruby` read as a user statement about the runtime: the configured
293
+ # version when the user set it explicitly, nil when it is the default or `"latest"`. `"latest"` names the
294
+ # newest syntax this Prism build parses, which moves with a Rigor upgrade, so it states no runtime. Only the
295
+ # Ruby-deprecation rules read this; `Inference::VersionGuard` keeps the analyzer's own `RUBY_VERSION`.
296
+ def stated_runtime_ruby
297
+ return nil unless target_ruby_explicit?
298
+ return nil if target_ruby == "latest"
299
+
300
+ target_ruby
301
+ end
302
+
285
303
  # ADR-103 WD13 — whether effect collection runs. True exactly when the loaded configuration carried an
286
304
  # `effects:` block, whatever its body; `rigor effects` enables it for its own run by loading an
287
305
  # implicit `effects: {}` instead of by consulting anything else. Nothing else — no annotation, no
@@ -307,7 +325,7 @@ module Rigor
307
325
  def resolved_test_paths(root: Dir.pwd)
308
326
  return test_paths unless test_paths.nil?
309
327
 
310
- CONVENTIONAL_TEST_PATHS.map { |dir| File.expand_path(dir, root) }.select { |dir| File.directory?(dir) }
328
+ CONVENTIONAL_TEST_PATHS.map { |dir| File.absolute_path(dir, root) }.select { |dir| File.directory?(dir) }
311
329
  end
312
330
 
313
331
  # Loads a configuration file.
@@ -326,6 +344,7 @@ module Rigor
326
344
  if resolved.nil? || !File.exist?(resolved)
327
345
  data = DEFAULTS
328
346
  effects_key_present = false
347
+ target_ruby_key_present = false
329
348
  else
330
349
  # ADR-103 WD15 — captured from the RAW, pre-`DEFAULTS.merge` file (+ its `includes:` chain) because
331
350
  # `DEFAULTS` itself carries an `"effects" => false` entry: once merged, "the file never wrote
@@ -333,9 +352,10 @@ module Rigor
333
352
  # indistinguishable to {#initialize}. This is the only place that distinction still exists.
334
353
  raw = load_with_includes(resolved)
335
354
  effects_key_present = raw.key?("effects")
355
+ target_ruby_key_present = raw.key?("target_ruby")
336
356
  data = DEFAULTS.merge(raw)
337
357
  end
338
- new(autowire_default_plugins(data), effects_key_present)
358
+ new(autowire_default_plugins(data), effects_key_present, target_ruby_key_present || false)
339
359
  end
340
360
 
341
361
  # ADR-93 WD2 — the one bundled plugin default-wired without a `plugins:` entry. `rigor-rbs-inline` is the
@@ -404,7 +424,7 @@ module Rigor
404
424
  # the include-aware load before applying `--treat-all-as-inline-rbs`'s plugin injection (see
405
425
  # {CLI::CheckCommand#load_check_configuration}).
406
426
  def self.load_with_includes(path, visited: Set.new)
407
- absolute = File.expand_path(path)
427
+ absolute = File.absolute_path(path)
408
428
  raise ConfigurationError, "circular include: #{absolute}" if visited.include?(absolute)
409
429
 
410
430
  raw = read_yaml(absolute)
@@ -433,7 +453,7 @@ module Rigor
433
453
 
434
454
  accumulated = {}
435
455
  includes.each do |inc|
436
- inc_path = File.expand_path(inc.to_s, base_dir)
456
+ inc_path = ConfigPath.absolute(inc.to_s, base_dir)
437
457
  unless File.exist?(inc_path)
438
458
  raise ConfigurationError, "include not found: #{inc.inspect} (referenced from #{base_dir})"
439
459
  end
@@ -459,7 +479,7 @@ module Rigor
459
479
  def self.resolve_path_key!(out, key, base_dir)
460
480
  return unless out.key?(key) && !out[key].nil?
461
481
 
462
- out[key] = Array(out[key]).map { |p| File.expand_path(p.to_s, base_dir) }
482
+ out[key] = Array(out[key]).map { |p| ConfigPath.absolute(p.to_s, base_dir) }
463
483
  end
464
484
 
465
485
  def self.resolve_plugins_io_paths!(out, base_dir)
@@ -467,7 +487,7 @@ module Rigor
467
487
  return unless plugins_io.is_a?(Hash) && plugins_io["allowed_paths"]
468
488
 
469
489
  duped = plugins_io.dup
470
- duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| File.expand_path(p.to_s, base_dir) }
490
+ duped["allowed_paths"] = Array(plugins_io["allowed_paths"]).map { |p| ConfigPath.absolute(p.to_s, base_dir) }
471
491
  out["plugins_io"] = duped
472
492
  end
473
493
 
@@ -518,7 +538,13 @@ module Rigor
518
538
  # `"key" => value` hash literal — this class's usual call shape, all over the spec suite — is coerced
519
539
  # into keyword arguments by Ruby whenever the method declares ANY keyword parameter, which would break
520
540
  # every `Configuration.new("some_key" => value)` call site with an "unknown keyword" `ArgumentError`.
521
- def initialize(data = DEFAULTS, effects_key_present = data.key?("effects"))
541
+ # @param target_ruby_key_present — ADR-47 WD5 amendment (#1692) — whether the file set `target_ruby:` itself,
542
+ # captured before `DEFAULTS.merge` for the same reason as `effects_key_present` and positional for the same
543
+ # reason. See {#stated_runtime_ruby}. The default reads the key off `data`, which is right only for a hash that
544
+ # has not been through `DEFAULTS.merge` (DEFAULTS carries the key); a caller that merges passes it explicitly.
545
+ # The bare `Configuration.new` is not explicit.
546
+ def initialize(data = DEFAULTS, effects_key_present = data.key?("effects"),
547
+ target_ruby_key_present = data.key?("target_ruby") && !data.equal?(DEFAULTS))
522
548
  # Record before the per-key fetches below discard the evidence. Top level only, deliberately —
523
549
  # see {ConfigAudit.unknown_key_warnings} for why a nested check cannot key on DEFAULTS.
524
550
  @unknown_keys = (data.keys.map(&:to_s) - KNOWN_KEYS).sort.freeze
@@ -527,6 +553,7 @@ module Rigor
527
553
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
528
554
 
529
555
  @target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
556
+ @target_ruby_explicit = target_ruby_key_present ? true : false
530
557
  @paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
531
558
  user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
532
559
  @exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
@@ -618,6 +645,9 @@ module Rigor
618
645
  def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
619
646
  {
620
647
  "target_ruby" => target_ruby,
648
+ # A stated `target_ruby` turns on the deprecation rules a defaulted one leaves off (#1692), so the run cache
649
+ # key and the incremental fingerprint must tell the two apart even where the value matches.
650
+ "target_ruby_explicit" => target_ruby_explicit?,
621
651
  "paths" => paths,
622
652
  "exclude" => exclude_patterns - BUILTIN_EXCLUDES,
623
653
  "plugins" => plugins,
@@ -716,6 +746,21 @@ module Rigor
716
746
  copy.freeze
717
747
  end
718
748
 
749
+ # #1777 — a sibling Configuration with only `baseline:` unset, every other field (including the
750
+ # explicitness of `target_ruby`) shared with the receiver. `rigor baseline generate` analyses with
751
+ # this so the generating run sees exactly the configuration `rigor check` will, minus the filter it
752
+ # is about to replace; rebuilding from a key list let each unlisted key revert to its default.
753
+ #
754
+ # Same frozen `dup` shape as {#with_bleeding_edge}. `@baseline_path` is the only ivar
755
+ # `#initialize` derives from the `baseline:` key.
756
+ def without_baseline
757
+ return self if baseline_path.nil?
758
+
759
+ copy = dup
760
+ copy.instance_variable_set(:@baseline_path, nil)
761
+ copy.freeze
762
+ end
763
+
719
764
  # ADR-103 WD14 — the ad-hoc opt-in `rigor effects` uses when the project's configuration carries no
720
765
  # `effects:` block: a sibling Configuration with an implicit empty block, every other field shared.
721
766
  # A configuration that already enables effects is returned unchanged, so a project's own settings
@@ -311,12 +311,12 @@ module Rigor
311
311
  # `[class_name, …ancestors]`, memoised per class. Cycle-guarded and capped: the tables are *as
312
312
  # written*, so a project can spell an ancestry that loops.
313
313
  #
314
- # Included and prepended modules are walked beside the superclass, nearer first, because that is
315
- # Ruby's own lookup order and because a framework's contract is as often a module as a base class:
316
- # a Sidekiq worker is `class TriggerWebhookWorker; include Sidekiq::Job`, with no base class at all,
317
- # so a superclass-only walk could not match a row about it however the plugin spelled one (#456).
318
- # `ExpressionTyper#enqueue_ancestors` already resolves a self-call this way; the two tables
319
- # disagreeing about what an ancestor is was the bug.
314
+ # Included and prepended modules are walked beside the superclass, nearer first, because a framework's
315
+ # contract is as often a module as a base class: a Sidekiq worker is `class TriggerWebhookWorker;
316
+ # include Sidekiq::Job`, with no base class at all, so a superclass-only walk could not match a row
317
+ # about it however the plugin spelled one (#456). Breadth-first is close to Ruby's lookup order but not
318
+ # it (a module an included module includes comes after the superclass here); `Scope::ResolutionChain`
319
+ # is the exact order, and this walk over the plugin facts' own copy of the tables does not read it yet.
320
320
  def ancestry(class_name)
321
321
  @ancestry[class_name] ||= begin
322
322
  chain = []
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
+ require_relative "../config_path"
6
+
5
7
  module Rigor
6
8
  class Environment
7
9
  # Target-project Bundler awareness (O4, implemented).
@@ -167,7 +169,7 @@ module Rigor
167
169
  # for the stats banner so end users can see what rigor picked up.
168
170
  def self.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true, home: nil)
169
171
  if bundle_path
170
- path = Pathname.new(File.expand_path(bundle_path.to_s, project_root))
172
+ path = Pathname.new(Rigor::ConfigPath.absolute(bundle_path.to_s, project_root))
171
173
  return path if path.directory?
172
174
 
173
175
  return nil
@@ -203,7 +205,7 @@ module Rigor
203
205
  # read of the real `~/.bundle/config`).
204
206
  def self.auto_detect(project_root:, home: nil)
205
207
  from_config = read_bundle_config_path(File.join(project_root, ".bundle", "config"))
206
- return File.expand_path(from_config, project_root) if from_config
208
+ return Rigor::ConfigPath.absolute(from_config, project_root) if from_config
207
209
 
208
210
  vendor = File.join(project_root, "vendor", "bundle")
209
211
  return vendor if File.directory?(vendor)
@@ -221,7 +223,7 @@ module Rigor
221
223
  configured = read_bundle_config_path(File.join(home, ".bundle", "config"))
222
224
  return nil unless configured
223
225
 
224
- resolved = File.expand_path(configured, project_root)
226
+ resolved = Rigor::ConfigPath.absolute(configured, project_root)
225
227
  File.directory?(resolved) ? resolved : nil
226
228
  end
227
229
  private_class_method :global_bundle_path
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ module Rigor
6
+ class Environment
7
+ # Issue #1700 — the type names a {RequiredFeatures}-gated vendored directory declares, which
8
+ # `RbsLoader.gated_trouble` builds in its trial to decide whether the directory may stay in an environment.
9
+ # Read off the vendored files themselves: a nested `class Prime; class EratosthenesGenerator` names
10
+ # `Prime::EratosthenesGenerator`, and a reopened core class (`class Integer`) is named too, since a clash
11
+ # there fails `Integer`'s own definition.
12
+ module GatedSignatureGuard
13
+ module_function
14
+
15
+ # @param dir — the vendored directory (Pathname).
16
+ # @return the absolute-less names (`"Prime::PseudoPrimeGenerator"`) of every class and module it declares.
17
+ def type_names(dir)
18
+ Dir.glob(File.join(dir.to_s, "*.rbs")).flat_map do |file|
19
+ _, _, decls = RBS::Parser.parse_signature(File.read(file))
20
+ names(decls, nil)
21
+ rescue RBS::BaseError, SystemCallError
22
+ []
23
+ end.uniq
24
+ end
25
+
26
+ def names(decls, namespace)
27
+ decls.flat_map do |decl|
28
+ next [] unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
29
+
30
+ name = qualify(namespace, decl.name)
31
+ [name] + names(decl.members, name)
32
+ end
33
+ end
34
+
35
+ def qualify(namespace, type_name)
36
+ text = type_name.to_s
37
+ return text.delete_prefix("::") if text.start_with?("::") || namespace.nil?
38
+
39
+ "#{namespace}::#{text}"
40
+ end
41
+ end
42
+ end
43
+ end