solargraph-rails 0.3.1 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdae0802262c6077a713ab7360faba89107df21b076feb137632d38ea119b7ab
4
- data.tar.gz: 241082c1dd11fd455da5b9c309d8836021e9509f7ecad08542d71307485cf726
3
+ metadata.gz: 134394397585a1df968b364e98d20b8e38a2fb6294833bfe8bbcfb81b13c545f
4
+ data.tar.gz: cb4e7fe2906a0769c95e8a3e7de08b6d0188b4dd8d509db2e001006e9367e6db
5
5
  SHA512:
6
- metadata.gz: 06c879380e6f6bc9cf0858809e725058d6b2d013db97816de6fc9afc23431bd51baede7431b1ed7e6ae3338707fe2978b90493119917ff0b3cecf51ccadbc925
7
- data.tar.gz: fbda10862a2f58edcd111f643d475111fdea59c0b2f5797f024365c7858642be78747a11ffd04511c35b73791e6e63bdbfa329e7a6dbce46e8fc96488bd3234d
6
+ metadata.gz: c7acc8b51be9a665bcca385a67bcee5eed0ffd9e697c155bfab4e5e94dcd691cb4ccab4f04c09306166c99e9802e3276e9895374540d3df7b3c979c59be582d4
7
+ data.tar.gz: 4de5589904080422ed72fec8d76e4679a53df812f551c7e99d5d0e6fc7a6cabefe5256a6403dab04ae96e0fd79ec85c92f7a171502202afdb7403e30b266ee04
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **Debug log**
27
+
28
+ Run the following command in the project you are having problems:
29
+ ```
30
+ ruby -r'solargraph-rails' -e 'Solargraph::Rails::Debug.run()'
31
+ ```
32
+
33
+ and paste the output here
@@ -0,0 +1,36 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ workflow_dispatch: {}
12
+ pull_request:
13
+ branches: [master]
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ matrix:
20
+ ruby-version:
21
+ - "2.7"
22
+ - "3.0"
23
+ - "3.1"
24
+
25
+ steps:
26
+ - uses: actions/checkout@v2
27
+ - uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby-version }}
30
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31
+ - name: Install Rails 5 deps
32
+ run: (cd spec/rails5; bundle install; yard gems)
33
+ - name: Install Rails 6 deps
34
+ run: (cd spec/rails6; bundle install; yard gems)
35
+ - name: Run tests
36
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -6,6 +6,13 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ .DS_Store
10
+ spec/rails5/log/
11
+ spec/rails5/tmp/
12
+ spec/rails6/log/
13
+ spec/rails6/tmp/
14
+ .projections.json
15
+ .byebug_history
9
16
 
10
17
  # rspec failure tracking
11
18
  .rspec_status
data/.solargraph.yml ADDED
@@ -0,0 +1,19 @@
1
+ ---
2
+ include:
3
+ - "**/*.rb"
4
+ exclude:
5
+ - test/**/*
6
+ - vendor/**/*
7
+ - ".bundle/**/*"
8
+ require: []
9
+ domains: []
10
+ reporters: []
11
+ formatter:
12
+ rubocop:
13
+ cops: safe
14
+ except: []
15
+ only: []
16
+ extra_args: []
17
+ require_paths: []
18
+ plugins: []
19
+ max_files: 5000
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Changes
4
4
 
5
+ ### v1.0.0.pre.1
6
+
7
+ https://github.com/alisnic/solargraph-arc was merged, with the following features:
8
+ - fixes autocompletion for multi-level classes defined in 1 line `class Foo::Bar::Baz`. See https://github.com/castwide/solargraph/issues/506
9
+ - autocomplete database columns by parsing db/schema.rb
10
+ - autocomplete of model relations
11
+ - parsing of `delegate` calls
12
+ - completions for methods generated by Devise
13
+ - better support for running solargraph outside bundle
14
+ - better completion inside controllers. `request`, `response`, `params`, etc.
15
+ - autocomplete inside routes.rb
16
+ - autocomplete inside migrations
17
+ - completions for methods generated by ActiveStorage
18
+ - better ActiveRecord completions
19
+
5
20
  ### v0.3.0
6
21
  * Require String inflection monkeypatches directly to avoid error from ActiveSupport v7
7
22
  * Remove Gemfile.lock from version control
data/DEVELOPMENT.md ADDED
@@ -0,0 +1,83 @@
1
+ # Solargraph-Rails development guide
2
+
3
+ ## Contributing
4
+
5
+ 1. create fork and clone the repo
6
+ 2. install gem deps `bundle install`
7
+ 3. install dummy rails5 app deps and build its yard cache
8
+
9
+ ```
10
+ $ cd spec/rails5
11
+ $ bundle install && yard gems
12
+ $ cd ../../
13
+ ```
14
+
15
+ 3. install dummy rails6 app deps and build its yard cache
16
+
17
+ ```
18
+ $ cd spec/rails6
19
+ $ bundle install && yard gems
20
+ $ cd ../../
21
+ ```
22
+ 4. now tests should pass locally and you can try different changes
23
+ 5. sumbit PR
24
+
25
+ ## Completion coverage tracking
26
+
27
+ Solargraph-Rails uses a [set of yaml files](https://github.com/iftheshoefritz/solargraph-rails/tree/master/spec/definitions) to track coverage of found completions.
28
+ Those yaml files are generated at runtime from a dummy [rails5](https://github.com/iftheshoefritz/solargraph-rails/tree/master/spec/rails5) or [rails6](https://github.com/iftheshoefritz/solargraph-rails/tree/master/spec/rails6) app.
29
+
30
+ The main goal is to catch any regressions in case of any change. In case a method completion is marked completed and it is not found in solargraph completions, the tests will fail.
31
+
32
+ ### Checking coverage
33
+
34
+ To see what is completion coverage for solargraph-rails, run the tests with the `PRINT_STATS=true` environment variable:
35
+
36
+ ```
37
+ $ PRINT_STATS=true bundle exec rspec
38
+ ```
39
+
40
+ What you will see in test output is reported coverage for classes that are tracked:
41
+
42
+ ```
43
+ {:class_name=>"ActiveRecord::Base", :total=>800, :covered=>321, :typed=>10, :percent_covered=>40.1, :percent_typed=>1.3}
44
+ provides completions for ActiveRecord::Base
45
+ ```
46
+
47
+ ### Updating assertions
48
+
49
+ In case an improvement is made, and more completions are found then being asserted, tests will throw a warning:
50
+
51
+ ```
52
+ ActionDispatch::Routing::Mapper.try! is marked as skipped in spec/definitions/rails5/routes.yml, but is actually present.
53
+ Consider setting skip=false
54
+ provides completions for ActionDispatch::Routing::Mapper
55
+ ```
56
+
57
+ In this case there are 2 options:
58
+ 1. Manually updating yml file and setting `skip: false` for that method
59
+ 2. Updating yml file in place by passing `update: true` to assertion:
60
+
61
+ ```diff
62
+ assert_matches_definition(
63
+ map,
64
+ 'ActionDispatch::Routing::Mapper',
65
+ 'rails5/routes',
66
+ + update: true
67
+ )
68
+ end
69
+ ```
70
+
71
+ In case of option 2, don't forget to remove the flag after yml file has been updated. Also review git diff, to make sure that no regressions have been set (skip=true was set for entries which previously had skip=false)
72
+
73
+ ### Generating assertions
74
+
75
+ In case a new set of assertion files has to be created (for a new Rails version for example), a script can be used - https://github.com/iftheshoefritz/solargraph-rails/blob/master/script/generate_definitions.rb.
76
+
77
+ All you have to do is to go to the same Rails app root, and execute the script:
78
+
79
+ ```
80
+ ruby path/to/generate_definitions.rb
81
+ ```
82
+
83
+ Make sure to review the script and uncomment relevant parts
@@ -0,0 +1,44 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Annotate
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def self.reset
9
+ @instance = nil
10
+ end
11
+
12
+ def initialize
13
+ @schema_present = File.exist?('db/schema.rb')
14
+ end
15
+
16
+ def process(source_map, ns)
17
+ return [] if @schema_present
18
+ return [] unless source_map.filename.include?('app/models')
19
+
20
+ pins = []
21
+ walker = Walker.from_source(source_map.source)
22
+ walker.comments.each do |_, snip|
23
+ name, type = snip.text.gsub(/[\(\),:\d]/, '').split[1..2]
24
+
25
+ next unless name && type
26
+
27
+ ruby_type = Schema::RUBY_TYPES[type.to_sym]
28
+ next unless ruby_type
29
+
30
+ pins <<
31
+ Util.build_public_method(
32
+ ns,
33
+ name,
34
+ types: [ruby_type],
35
+ location:
36
+ Solargraph::Location.new(source_map.filename, snip.range)
37
+ )
38
+ end
39
+
40
+ pins
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ # The following comments fill some of the gaps in Solargraph's understanding of
2
+ # Rails apps. Since they're all in YARD, they get mapped in Solargraph but
3
+ # ignored at runtime.
4
+ #
5
+ # You can put this file anywhere in the project, as long as it gets included in
6
+ # the workspace maps. It's recommended that you keep it in a standalone file
7
+ # instead of pasting it into an existing one.
8
+ #
9
+ # @!parse
10
+ # class ActionController::Base
11
+ # include ActionController::MimeResponds
12
+ # include ActionController::Redirecting
13
+ # include ActionController::Cookies
14
+ # include AbstractController::Rendering
15
+ # extend ActiveSupport::Callbacks::ClassMethods
16
+ # extend ActiveSupport::Rescuable::ClassMethods
17
+ # extend AbstractController::Callbacks::ClassMethods
18
+ # extend ActionController::RequestForgeryProtection::ClassMethods
19
+ # end
20
+ # class ActionDispatch::Routing::Mapper
21
+ # include ActionDispatch::Routing::Mapper::Base
22
+ # include ActionDispatch::Routing::Mapper::HttpHelpers
23
+ # include ActionDispatch::Routing::Mapper::Redirection
24
+ # include ActionDispatch::Routing::Mapper::Scoping
25
+ # include ActionDispatch::Routing::Mapper::Concerns
26
+ # include ActionDispatch::Routing::Mapper::Resources
27
+ # include ActionDispatch::Routing::Mapper::CustomUrls
28
+ # end
29
+ # class Rails
30
+ # # @return [Rails::Application]
31
+ # def self.application; end
32
+ # end
33
+ # class Rails::Application
34
+ # # @return [ActionDispatch::Routing::RouteSet]
35
+ # def routes; end
36
+ # end
37
+ # class ActionDispatch::Routing::RouteSet
38
+ # # @yieldself [ActionDispatch::Routing::Mapper]
39
+ # def draw; end
40
+ # end
41
+ # class ActiveRecord::Base
42
+ # extend ActiveRecord::QueryMethods
43
+ # extend ActiveRecord::FinderMethods
44
+ # extend ActiveRecord::Associations::ClassMethods
45
+ # extend ActiveRecord::Inheritance::ClassMethods
46
+ # extend ActiveRecord::ModelSchema::ClassMethods
47
+ # extend ActiveRecord::Transactions::ClassMethods
48
+ # extend ActiveRecord::Scoping::Named::ClassMethods
49
+ # include ActiveRecord::Persistence
50
+ # end
@@ -0,0 +1,53 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Autoload
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns, ds)
9
+ return [] unless ds.size == 1 && ns.path.include?('::')
10
+ Solargraph.logger.debug(
11
+ "[Rails][Autoload] seeding class tree for #{ns.path}"
12
+ )
13
+
14
+ root_ns = source_map.pins.find { |p| p.path == '' }
15
+ namespace_stubs(root_ns, ns)
16
+ end
17
+
18
+ def namespace_stubs(root_ns, ns)
19
+ parts = ns.path.split('::')
20
+
21
+ candidates =
22
+ parts
23
+ .each_with_index
24
+ .reduce([]) { |acc, (_, i)| acc + [parts[0..i].join('::')] }
25
+ .reject { |el| el == ns.path }
26
+
27
+ previous_ns = root_ns
28
+ pins = []
29
+
30
+ parts[0..-2].each_with_index do |name, i|
31
+ gates = candidates[0..i].reverse + ['']
32
+ path = gates.first
33
+ next if path == ns.path
34
+
35
+ previous_ns =
36
+ Solargraph::Pin::Namespace.new(
37
+ type: :class,
38
+ location: ns.location,
39
+ closure: previous_ns,
40
+ name: name,
41
+ comments: ns.comments,
42
+ visibility: :public,
43
+ gates: gates[1..-1]
44
+ )
45
+
46
+ pins << previous_ns
47
+ end
48
+
49
+ pins
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Debug
4
+ def self.run(query = nil)
5
+ self.new.run(query)
6
+ end
7
+
8
+ def run(query)
9
+ Solargraph.logger.level = Logger::DEBUG
10
+
11
+ api_map = Solargraph::ApiMap.load('./')
12
+
13
+ puts "Ruby version: #{RUBY_VERSION}"
14
+ puts "Solargraph version: #{Solargraph::VERSION}"
15
+ puts "Solargraph Rails version: #{Solargraph::Rails::VERSION}"
16
+
17
+ return unless query
18
+
19
+ puts "Known methods for #{query}"
20
+
21
+ pin = api_map.pins.find { |p| p.path == query }
22
+ return unless pin
23
+
24
+ api_map
25
+ .get_complex_type_methods(pin.return_type)
26
+ .each { |pin| puts "- #{pin.path}" }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Delegate
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ return [] unless source_map.code.include?('delegate')
10
+
11
+ walker = Walker.from_source(source_map.source)
12
+ pins = []
13
+
14
+ walker.on :send, [nil, :delegate] do |ast|
15
+ methods =
16
+ ast.children[2..-1]
17
+ .map { |c| c.children.first }
18
+ .select { |s| s.is_a?(Symbol) }
19
+
20
+ methods.each do |meth|
21
+ pins <<
22
+ Util.build_public_method(
23
+ ns,
24
+ meth.to_s,
25
+ location: Util.build_location(ast, ns.filename)
26
+ )
27
+ end
28
+ end
29
+
30
+ walker.walk
31
+
32
+ if pins.any?
33
+ Solargraph.logger.debug(
34
+ "[Rails][Delegate] added #{pins.map(&:name)} to #{ns.path}"
35
+ )
36
+ end
37
+
38
+ pins
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Devise
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ if source_map.filename.include?('app/models')
10
+ process_model(source_map.source, ns)
11
+ else
12
+ []
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def process_model(source, ns)
19
+ walker = Walker.from_source(source)
20
+ pins = []
21
+
22
+ walker.on :send, [nil, :devise] do |ast|
23
+ modules =
24
+ ast.children[2..-1]
25
+ .map { |c| c.children.first }
26
+ .select { |s| s.is_a?(Symbol) }
27
+
28
+ modules.each do |mod|
29
+ pins <<
30
+ Util.build_module_include(
31
+ ns,
32
+ "Devise::Models::#{mod.to_s.capitalize}",
33
+ Util.build_location(ast, ns.filename)
34
+ )
35
+ end
36
+ end
37
+
38
+ walker.walk
39
+ if pins.any?
40
+ Solargraph.logger.debug(
41
+ "[Rails][Devise] added #{pins.map(&:name)} to #{ns.path}"
42
+ )
43
+ end
44
+ pins
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,100 @@
1
+ module Solargraph
2
+ module Rails
3
+ class Model
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ return [] unless source_map.filename.include?('app/models')
10
+
11
+ walker = Walker.from_source(source_map.source)
12
+ pins = []
13
+
14
+ walker.on :send, [nil, :belongs_to] do |ast|
15
+ pins << singular_association(ns, ast)
16
+ end
17
+
18
+ walker.on :send, [nil, :has_one] do |ast|
19
+ pins << singular_association(ns, ast)
20
+ end
21
+
22
+ walker.on :send, [nil, :has_many] do |ast|
23
+ pins << plural_association(ns, ast)
24
+ end
25
+
26
+ walker.on :send, [nil, :has_and_belongs_to_many] do |ast|
27
+ pins << plural_association(ns, ast)
28
+ end
29
+
30
+ walker.on :send, [nil, :scope] do |ast|
31
+ name = ast.children[2].children.last
32
+
33
+ method_pin =
34
+ Util.build_public_method(
35
+ ns,
36
+ name.to_s,
37
+ types: ns.return_type.map(&:tag),
38
+ scope: :class,
39
+ location: Util.build_location(ast, ns.filename)
40
+ )
41
+
42
+ if ast.children.last.type == :block
43
+ location = ast.children.last.location
44
+ block_pin =
45
+ source_map.locate_block_pin(location.line, location.column)
46
+ method_pin.parameters.concat(block_pin.parameters.clone)
47
+ end
48
+ pins << method_pin
49
+ end
50
+
51
+ walker.walk
52
+ if pins.any?
53
+ Solargraph.logger.debug(
54
+ "[Rails][Model] added #{pins.map(&:name)} to #{ns.path}"
55
+ )
56
+ end
57
+ pins
58
+ end
59
+
60
+ def plural_association(ns, ast)
61
+ relation_name = ast.children[2].children.first
62
+ class_name =
63
+ extract_custom_class_name(ast) ||
64
+ relation_name.to_s.singularize.camelize
65
+
66
+ Util.build_public_method(
67
+ ns,
68
+ relation_name.to_s,
69
+ types: ["ActiveRecord::Associations::CollectionProxy<#{class_name}>"],
70
+ location: Util.build_location(ast, ns.filename)
71
+ )
72
+ end
73
+
74
+ def singular_association(ns, ast)
75
+ relation_name = ast.children[2].children.first
76
+ class_name =
77
+ extract_custom_class_name(ast) || relation_name.to_s.camelize
78
+
79
+ Util.build_public_method(
80
+ ns,
81
+ relation_name.to_s,
82
+ types: [class_name],
83
+ location: Util.build_location(ast, ns.filename)
84
+ )
85
+ end
86
+
87
+ def extract_custom_class_name(ast)
88
+ options = ast.children[3..-1].find { |n| n.type == :hash }
89
+ return unless options
90
+
91
+ class_name_pair =
92
+ options.children.find do |n|
93
+ n.children[0].deconstruct == %i[sym class_name] &&
94
+ n.children[1].type == :str
95
+ end
96
+ class_name_pair && class_name_pair.children.last.children.last
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,98 @@
1
+ module Solargraph
2
+ module Rails
3
+ class RailsApi
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def global(yard_map)
9
+ return [] if yard_map.required.empty?
10
+
11
+ ann = File.read(File.dirname(__FILE__) + '/annotations.rb')
12
+ source = Solargraph::Source.load_string(ann, 'annotations.rb')
13
+ map = Solargraph::SourceMap.map(source)
14
+
15
+ Solargraph.logger.debug(
16
+ "[Rails][Rails] found #{map.pins.size} pins in annotations"
17
+ )
18
+
19
+ overrides =
20
+ YAML
21
+ .load_file(File.dirname(__FILE__) + '/types.yml')
22
+ .map do |meth, data|
23
+ if data['return']
24
+ Util.method_return(meth, data['return'])
25
+ elsif data['yieldself']
26
+ Solargraph::Pin::Reference::Override.from_comment(
27
+ meth,
28
+ "@yieldself [#{data['yieldself'].join(',')}]"
29
+ )
30
+ elsif data['yieldparam']
31
+ Solargraph::Pin::Reference::Override.from_comment(
32
+ meth,
33
+ "@yieldparam [#{data['yieldparam'].join(',')}]"
34
+ )
35
+ end
36
+ end
37
+
38
+ ns =
39
+ Solargraph::Pin::Namespace.new(
40
+ name: 'ActionController::Base',
41
+ gates: ['ActionController::Base']
42
+ )
43
+
44
+ definitions = [
45
+ Util.build_public_method(
46
+ ns,
47
+ 'response',
48
+ types: ['ActionDispatch::Response'],
49
+ location: Util.dummy_location('whatever.rb')
50
+ ),
51
+ Util.build_public_method(
52
+ ns,
53
+ 'request',
54
+ types: ['ActionDispatch::Request'],
55
+ location: Util.dummy_location('whatever.rb')
56
+ ),
57
+ Util.build_public_method(
58
+ ns,
59
+ 'session',
60
+ types: ['ActionDispatch::Request::Session'],
61
+ location: Util.dummy_location('whatever.rb')
62
+ ),
63
+ Util.build_public_method(
64
+ ns,
65
+ 'flash',
66
+ types: ['ActionDispatch::Flash::FlashHash'],
67
+ location: Util.dummy_location('whatever.rb')
68
+ )
69
+ ]
70
+
71
+ map.pins + definitions + overrides
72
+ end
73
+
74
+ def local(source_map, ns)
75
+ return [] unless source_map.filename.include?('db/migrate')
76
+ node, _ = Walker.normalize_ast(source_map.source)
77
+
78
+ pins = [
79
+ Util.build_module_include(
80
+ ns,
81
+ 'ActiveRecord::ConnectionAdapters::SchemaStatements',
82
+ Util.build_location(node, ns.filename)
83
+ ),
84
+ Util.build_module_extend(
85
+ ns,
86
+ 'ActiveRecord::ConnectionAdapters::SchemaStatements',
87
+ Util.build_location(node, ns.filename)
88
+ )
89
+ ]
90
+
91
+ Solargraph.logger.debug(
92
+ "[Rails][RailsApi] added #{pins.map(&:name)} to #{ns.path}"
93
+ )
94
+ pins
95
+ end
96
+ end
97
+ end
98
+ end