smart_todo 1.2.0 → 1.3.0

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: f43df881f5ab5894069683abbe3d09571232a896a429d0d2795d57e8102e42dc
4
- data.tar.gz: 070dc363e6e1aa44e605e1fe53126bfc613af23985cfc7cdb48a145fe93362e0
3
+ metadata.gz: 8e7fe4f3876be333b1df4dcd44d965e57a0725e65900f96a518bd6a49d608165
4
+ data.tar.gz: 02b5370dfc45cc2337d973db79a3a6bddccb8ae295d2ec529eebbb166247873b
5
5
  SHA512:
6
- metadata.gz: 69d9f4f170b854769a3141a41135c25463b2839274be7a6cea062b2d524fa5cee37e64c0a134f9de10944b9fce9e3580ed8f3b5ca4c8a8101528c3f9691a1c36
7
- data.tar.gz: c5b66f439a2176728ca2f8ef4297fceeaa6d9f08c1ba190d4a255b8e3164264262536681d34a9cab4a9837c2455e75ecdf1dd9dc6b7b8b9877e37ef026719c01
6
+ metadata.gz: b1bcacca903ebeb350b7abbb8af580de31904bae313c08aac9ffd53351ea8d13897bda85d6ede9e12971dcf5fa3f5a68b2ebb1f71bcbfddd443c34e6ee6c65fc
7
+ data.tar.gz: 970874dccc0323c3b009ec22a083c5096172390fe7c34bb698958cd9e89deb958f5be69dc26145c945a1aac7d740875656ea8a9eb0372dfecbd7ca04745a0a2a
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ name: Ruby ${{ matrix.version }}
13
+ strategy:
14
+ matrix:
15
+ version: [2.5, 2.6, 2.7, 3.0]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - name: Set up Ruby ${{ matrix.version }}
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.version }}
23
+ bundler-cache: true
24
+ - name: Run Tests
25
+ run: |
26
+ bundle exec rake test
27
+
@@ -0,0 +1,22 @@
1
+ name: RuboCop
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v2
11
+ - name: Set up Ruby 3.0
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: 3.0
15
+ bundler-cache: true
16
+ - name: Install gems
17
+ run: |
18
+ bundle config path vendor/bundle
19
+ bundle config set without 'default development test'
20
+ bundle install --jobs 4 --retry 3
21
+ - name: Run RuboCop
22
+ run: bundle exec rubocop --parallel
data/.rubocop.yml CHANGED
@@ -1,7 +1,7 @@
1
- inherit_from:
2
- - http://shopify.github.io/ruby-style-guide/rubocop.yml
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.5
5
+ TargetRubyVersion: 3.0
6
6
  Exclude:
7
7
  - vendor/**/*
data/Gemfile CHANGED
@@ -6,6 +6,6 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  gemspec
8
8
 
9
- group :development do
10
- gem 'rubocop', '~> 0.71'
9
+ group :rubocop do
10
+ gem "rubocop-shopify", require: false
11
11
  end
data/Gemfile.lock CHANGED
@@ -1,36 +1,43 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_todo (1.2.0)
4
+ smart_todo (1.3.0)
5
+ rexml
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
- addressable (2.6.0)
10
- public_suffix (>= 2.0.2, < 4.0)
11
- ast (2.4.0)
12
- crack (0.4.3)
13
- safe_yaml (~> 1.0.0)
14
- hashdiff (0.4.0)
15
- jaro_winkler (1.5.3)
16
- minitest (5.11.3)
17
- parallel (1.17.0)
18
- parser (2.6.3.0)
19
- ast (~> 2.4.0)
20
- public_suffix (3.1.1)
10
+ addressable (2.8.0)
11
+ public_suffix (>= 2.0.2, < 5.0)
12
+ ast (2.4.2)
13
+ crack (0.4.5)
14
+ rexml
15
+ hashdiff (1.0.1)
16
+ minitest (5.14.4)
17
+ parallel (1.21.0)
18
+ parser (3.0.2.0)
19
+ ast (~> 2.4.1)
20
+ public_suffix (4.0.6)
21
21
  rainbow (3.0.0)
22
- rake (10.5.0)
23
- rubocop (0.71.0)
24
- jaro_winkler (~> 1.5.1)
22
+ rake (13.0.6)
23
+ regexp_parser (2.1.1)
24
+ rexml (3.2.5)
25
+ rubocop (1.23.0)
25
26
  parallel (~> 1.10)
26
- parser (>= 2.6)
27
+ parser (>= 3.0.0.0)
27
28
  rainbow (>= 2.2.2, < 4.0)
29
+ regexp_parser (>= 1.8, < 3.0)
30
+ rexml
31
+ rubocop-ast (>= 1.12.0, < 2.0)
28
32
  ruby-progressbar (~> 1.7)
29
- unicode-display_width (>= 1.4.0, < 1.7)
30
- ruby-progressbar (1.10.1)
31
- safe_yaml (1.0.5)
32
- unicode-display_width (1.6.0)
33
- webmock (3.6.0)
33
+ unicode-display_width (>= 1.4.0, < 3.0)
34
+ rubocop-ast (1.13.0)
35
+ parser (>= 3.0.1.1)
36
+ rubocop-shopify (2.3.0)
37
+ rubocop (~> 1.22)
38
+ ruby-progressbar (1.11.0)
39
+ unicode-display_width (2.1.0)
40
+ webmock (3.11.2)
34
41
  addressable (>= 2.3.6)
35
42
  crack (>= 0.3.2)
36
43
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -39,12 +46,12 @@ PLATFORMS
39
46
  ruby
40
47
 
41
48
  DEPENDENCIES
42
- bundler (~> 1.17)
49
+ bundler (>= 1.17)
43
50
  minitest (~> 5.0)
44
- rake (~> 10.0)
45
- rubocop (~> 0.71)
51
+ rake (>= 10.0)
52
+ rubocop-shopify
46
53
  smart_todo!
47
54
  webmock
48
55
 
49
56
  BUNDLED WITH
50
- 1.17.3
57
+ 2.2.25
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="https://user-images.githubusercontent.com/8122246/61341925-b936d180-a848-11e9-95c1-0d2f398c51b1.png?raw=true" width="200">
3
3
  </h3>
4
4
 
5
- [![Build Status](https://travis-ci.com/Shopify/smart_todo.svg?branch=master)](https://travis-ci.com/Shopify/smart_todo)
5
+ [![Build Status](https://github.com/Shopify/smart_todo/workflows/CI/badge.svg)](https://github.com/Shopify/smart_todo/actions?query=workflow%3ACI)
6
6
 
7
7
  _SmartTodo_ is a library designed to assign users on TODO comments written in your codebase and help assignees be reminded when it's time to commit to their TODO.
8
8
 
data/dev.yml CHANGED
@@ -4,7 +4,7 @@ type:
4
4
  - ruby
5
5
 
6
6
  up:
7
- - ruby: 2.5.5
7
+ - ruby: 2.7.4
8
8
  - bundler
9
9
 
10
10
  test:
data/exe/smart_todo CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  $LOAD_PATH.unshift("#{__dir__}/../lib")
5
5
 
6
- require 'smart_todo'
6
+ require "smart_todo"
7
7
 
8
- if ENV['ENABLE_SMART_TODO'] && !ARGV.include?('--dispatcher')
9
- ARGV << '--dispatcher' << 'slack'
8
+ if ENV["ENABLE_SMART_TODO"] && !ARGV.include?("--dispatcher")
9
+ ARGV << "--dispatcher" << "slack"
10
10
  end
11
11
 
12
12
  SmartTodo::CLI.new.run
@@ -15,13 +15,13 @@ module SmartTodo
15
15
  def run(args = ARGV)
16
16
  paths = define_options.parse!(args)
17
17
  validate_options!
18
- paths << '.' if paths.empty?
18
+ paths << "." if paths.empty?
19
19
 
20
20
  paths.each do |path|
21
21
  normalize_path(path).each do |file|
22
22
  parse_file(file)
23
23
 
24
- STDOUT.print('.')
24
+ STDOUT.print(".")
25
25
  STDOUT.flush
26
26
  end
27
27
  end
@@ -38,13 +38,13 @@ module SmartTodo
38
38
  def define_options
39
39
  OptionParser.new do |opts|
40
40
  opts.banner = "Usage: smart_todo [options] file_or_path1 file_or_path2 ..."
41
- opts.on('--slack_token TOKEN') do |token|
41
+ opts.on("--slack_token TOKEN") do |token|
42
42
  @options[:slack_token] = token
43
43
  end
44
- opts.on('--fallback_channel CHANNEL') do |channel|
44
+ opts.on("--fallback_channel CHANNEL") do |channel|
45
45
  @options[:fallback_channel] = channel
46
46
  end
47
- opts.on('--dispatcher DISPATCHER') do |dispatcher|
47
+ opts.on("--dispatcher DISPATCHER") do |dispatcher|
48
48
  @options[:dispatcher] = dispatcher
49
49
  end
50
50
  end
@@ -67,7 +67,7 @@ module SmartTodo
67
67
 
68
68
  # @param file [String] a path to a file
69
69
  def parse_file(file)
70
- Parser::CommentParser.new(File.read(file, encoding: 'UTF-8')).parse.each do |todo_node|
70
+ Parser::CommentParser.new(File.read(file, encoding: "UTF-8")).parse.each do |todo_node|
71
71
  event_message = nil
72
72
  event_met = todo_node.metadata.events.find do |event|
73
73
  event_message = Events.public_send(event.method_name, *event.arguments)
@@ -12,7 +12,7 @@ module SmartTodo
12
12
  case dispatcher
13
13
  when "slack"
14
14
  Slack
15
- when nil, 'output'
15
+ when nil, "output"
16
16
  Output
17
17
  end
18
18
  end
@@ -25,7 +25,7 @@ module SmartTodo
25
25
  #
26
26
  # @return void
27
27
  def self.validate_options!(_options)
28
- raise(NotImplemetedError, 'subclass responsability')
28
+ raise(NotImplemetedError, "subclass responsability")
29
29
  end
30
30
 
31
31
  # @param event_message [String] the success message associated
@@ -38,7 +38,7 @@ module SmartTodo
38
38
  @todo_node = todo_node
39
39
  @options = options
40
40
  @file = file
41
- @assignee = @todo_node.metadata.assignee
41
+ @assignees = @todo_node.metadata.assignees
42
42
  end
43
43
 
44
44
  # This method gets called when a TODO reminder is expired and needs to be delivered.
@@ -46,7 +46,7 @@ module SmartTodo
46
46
  #
47
47
  # @return void
48
48
  def dispatch
49
- raise(NotImplemetedError, 'subclass responsability')
49
+ raise(NotImplemetedError, "subclass responsability")
50
50
  end
51
51
 
52
52
  private
@@ -54,10 +54,11 @@ module SmartTodo
54
54
  # Prepare the content of the message to send to the TODO assignee
55
55
  #
56
56
  # @param user [Hash] contain information about a user
57
+ # @param assignee [String] original string handle the slack message should be sent
57
58
  # @return [String]
58
- def slack_message(user)
59
- header = if user.key?('fallback')
60
- unexisting_user
59
+ def slack_message(user, assignee)
60
+ header = if user.key?("fallback")
61
+ unexisting_user(assignee)
61
62
  else
62
63
  existing_user
63
64
  end
@@ -78,12 +79,13 @@ module SmartTodo
78
79
 
79
80
  # Message in case a TODO's assignee doesn't exist in the Slack organization
80
81
  #
82
+ # @param user [Hash]
81
83
  # @return [String]
82
- def unexisting_user
83
- "Hello :wave:,\n\n`#{@assignee}` had an assigned TODO but this user or channel doesn't exist on Slack anymore."
84
+ def unexisting_user(assignee)
85
+ "Hello :wave:,\n\n`#{assignee}` had an assigned TODO but this user or channel doesn't exist on Slack anymore."
84
86
  end
85
87
 
86
- # @param user [Hash]
88
+ # Hello message for user actually existing in the organization
87
89
  def existing_user
88
90
  "Hello :wave:,"
89
91
  end
@@ -6,9 +6,21 @@ module SmartTodo
6
6
  # (using the associated slack email address) or a channel.
7
7
  class Slack < Base
8
8
  def self.validate_options!(options)
9
- options[:slack_token] ||= ENV.fetch('SMART_TODO_SLACK_TOKEN') { raise(ArgumentError, 'Missing :slack_token') }
9
+ options[:slack_token] ||= ENV.fetch("SMART_TODO_SLACK_TOKEN") { raise(ArgumentError, "Missing :slack_token") }
10
10
 
11
- options.fetch(:fallback_channel) { raise(ArgumentError, 'Missing :fallback_channel') }
11
+ options.fetch(:fallback_channel) { raise(ArgumentError, "Missing :fallback_channel") }
12
+ end
13
+
14
+ # Make a Slack API call to dispatch the message to each assignee
15
+ #
16
+ # @raise [SlackClient::Error] in case the Slack API returns an error
17
+ # other than `users_not_found`
18
+ #
19
+ # @return [Array] Slack response for each assignee a message was sent to
20
+ def dispatch
21
+ @assignees.each do |assignee|
22
+ dispatch_one(assignee)
23
+ end
12
24
  end
13
25
 
14
26
  # Make a Slack API call to dispatch the message to the user or channel
@@ -16,19 +28,20 @@ module SmartTodo
16
28
  # @raise [SlackClient::Error] in case the Slack API returns an error
17
29
  # other than `users_not_found`
18
30
  #
31
+ # @param [String] the assignee handle string
19
32
  # @return [Hash] the Slack response
20
- def dispatch
21
- user = slack_user_or_channel
33
+ def dispatch_one(assignee)
34
+ user = slack_user_or_channel(assignee)
22
35
 
23
- client.post_message(user.dig('user', 'id'), slack_message(user))
36
+ client.post_message(user.dig("user", "id"), slack_message(user, assignee))
24
37
  rescue SlackClient::Error => error
25
- if %w(users_not_found channel_not_found).include?(error.error_code)
26
- user = { 'user' => { 'id' => @options[:fallback_channel] }, 'fallback' => true }
38
+ if ["users_not_found", "channel_not_found"].include?(error.error_code)
39
+ user = { "user" => { "id" => @options[:fallback_channel] }, "fallback" => true }
27
40
  else
28
41
  raise(error)
29
42
  end
30
43
 
31
- client.post_message(user.dig('user', 'id'), slack_message(user))
44
+ client.post_message(user.dig("user", "id"), slack_message(user, assignee))
32
45
  end
33
46
 
34
47
  private
@@ -37,11 +50,11 @@ module SmartTodo
37
50
  # the channel the message should be sent to.
38
51
  #
39
52
  # @return [Hash] a suited hash containing the user ID for a given individual or a slack channel
40
- def slack_user_or_channel
41
- if email?
42
- client.lookup_user_by_email(@assignee)
53
+ def slack_user_or_channel(assignee)
54
+ if assignee.include?("@")
55
+ client.lookup_user_by_email(assignee)
43
56
  else
44
- { 'user' => { 'id' => @assignee } }
57
+ { "user" => { "id" => assignee } }
45
58
  end
46
59
  end
47
60
 
@@ -49,13 +62,6 @@ module SmartTodo
49
62
  def client
50
63
  @client ||= SlackClient.new(@options[:slack_token])
51
64
  end
52
-
53
- # Check if the TODO's assignee is a specific user or a channel
54
- #
55
- # @return [true, false]
56
- def email?
57
- @assignee.include?("@")
58
- end
59
65
  end
60
66
  end
61
67
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
3
+ require "time"
4
4
 
5
5
  module SmartTodo
6
6
  module Events
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem('bundler')
4
- require 'bundler'
3
+ gem("bundler")
4
+ require "bundler"
5
5
 
6
6
  module SmartTodo
7
7
  module Events
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
3
+ require "net/http"
4
+ require "json"
5
5
 
6
6
  module SmartTodo
7
7
  module Events
@@ -30,7 +30,7 @@ module SmartTodo
30
30
  if response.code_type < Net::HTTPClientError
31
31
  error_message
32
32
  elsif (gem = version_released?(response.body))
33
- message(gem['number'])
33
+ message(gem["number"])
34
34
  else
35
35
  false
36
36
  end
@@ -54,13 +54,13 @@ module SmartTodo
54
54
  # @return [true, false]
55
55
  def version_released?(gem_versions)
56
56
  JSON.parse(gem_versions).find do |gem|
57
- @requirements.satisfied_by?(Gem::Version.new(gem['number']))
57
+ @requirements.satisfied_by?(Gem::Version.new(gem["number"]))
58
58
  end
59
59
  end
60
60
 
61
61
  # @return [Net::HTTP] an instance of Net::HTTP
62
62
  def client
63
- @client ||= Net::HTTP.new('rubygems.org', Net::HTTP.https_default_port).tap do |client|
63
+ @client ||= Net::HTTP.new("rubygems.org", Net::HTTP.https_default_port).tap do |client|
64
64
  client.use_ssl = true
65
65
  end
66
66
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'json'
3
+ require "net/http"
4
+ require "json"
5
5
 
6
6
  module SmartTodo
7
7
  module Events
@@ -12,7 +12,7 @@ module SmartTodo
12
12
  # with the `repos` scope in the +SMART_TODO_GITHUB_TOKEN+ environment variable
13
13
  # is required.
14
14
  class IssueClose
15
- TOKEN_ENV = 'SMART_TODO_GITHUB_TOKEN'
15
+ TOKEN_ENV = "SMART_TODO_GITHUB_TOKEN"
16
16
 
17
17
  # @param organization [String]
18
18
  # @param repo [String]
@@ -62,7 +62,7 @@ module SmartTodo
62
62
 
63
63
  # @return [Net::HTTP] an instance of Net::HTTP
64
64
  def client
65
- @client ||= Net::HTTP.new('api.github.com', Net::HTTP.https_default_port).tap do |client|
65
+ @client ||= Net::HTTP.new("api.github.com", Net::HTTP.https_default_port).tap do |client|
66
66
  client.use_ssl = true
67
67
  end
68
68
  end
@@ -72,13 +72,13 @@ module SmartTodo
72
72
  #
73
73
  # @return [true, false]
74
74
  def pull_request_closed?(pull_request)
75
- JSON.parse(pull_request)['state'] == 'closed'
75
+ JSON.parse(pull_request)["state"] == "closed"
76
76
  end
77
77
 
78
78
  # @return [Hash]
79
79
  def default_headers
80
- { 'Accept' => 'application/vnd.github.v3+json' }.tap do |headers|
81
- headers['Authorization'] = "token #{ENV[TOKEN_ENV]}" if ENV[TOKEN_ENV]
80
+ { "Accept" => "application/vnd.github.v3+json" }.tap do |headers|
81
+ headers["Authorization"] = "token #{ENV[TOKEN_ENV]}" if ENV[TOKEN_ENV]
82
82
  end
83
83
  end
84
84
  end
@@ -54,7 +54,7 @@ module SmartTodo
54
54
  # @param issue_number [String, Integer]
55
55
  # @return [false, String]
56
56
  def issue_close(organization, repo, issue_number)
57
- IssueClose.new(organization, repo, issue_number, type: 'issues').met?
57
+ IssueClose.new(organization, repo, issue_number, type: "issues").met?
58
58
  end
59
59
 
60
60
  # Check if the pull request +pr_number+ is closed
@@ -64,7 +64,7 @@ module SmartTodo
64
64
  # @param pr_number [String, Integer]
65
65
  # @return [false, String]
66
66
  def pull_request_close(organization, repo, pr_number)
67
- IssueClose.new(organization, repo, pr_number, type: 'pulls').met?
67
+ IssueClose.new(organization, repo, pr_number, type: "pulls").met?
68
68
  end
69
69
  end
70
70
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ripper'
3
+ require "ripper"
4
4
 
5
5
  module SmartTodo
6
6
  module Parser
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ripper'
3
+ require "ripper"
4
4
 
5
5
  module SmartTodo
6
6
  module Parser
@@ -35,7 +35,7 @@ module SmartTodo
35
35
  # @param args [Array]
36
36
  # @return [Array, MethodNode]
37
37
  def on_method_add_arg(method, args)
38
- if method == 'TODO'
38
+ if method == "TODO"
39
39
  args
40
40
  else
41
41
  MethodNode.new(method, args)
@@ -58,12 +58,12 @@ module SmartTodo
58
58
  # @param key [String]
59
59
  # @param value [String, Integer, MethodNode]
60
60
  def on_assoc_new(key, value)
61
- key.tr!(':', '')
61
+ key.tr!(":", "")
62
62
 
63
63
  case key
64
- when 'on'
64
+ when "on"
65
65
  [:on_todo_event, value]
66
- when 'to'
66
+ when "to"
67
67
  [:on_todo_assignee, value]
68
68
  else
69
69
  [:unknown, value]
@@ -78,10 +78,11 @@ module SmartTodo
78
78
  end
79
79
 
80
80
  class Visitor
81
- attr_reader :events, :assignee
81
+ attr_reader :events, :assignees
82
82
 
83
83
  def initialize
84
84
  @events = []
85
+ @assignees = []
85
86
  end
86
87
 
87
88
  # Iterate over each tokens returned from the parser and call
@@ -111,7 +112,7 @@ module SmartTodo
111
112
  # @param assignee [String]
112
113
  # @return [void]
113
114
  def on_todo_assignee(assignee)
114
- @assignee = assignee
115
+ @assignees << assignee
115
116
  end
116
117
  end
117
118
  end
@@ -11,7 +11,7 @@ module SmartTodo
11
11
 
12
12
  # @param todo [String] the actual Ruby comment
13
13
  def initialize(todo)
14
- @metadata = MetadataParser.parse(todo.gsub(/^#/, ''))
14
+ @metadata = MetadataParser.parse(todo.gsub(/^#/, ""))
15
15
  @comments = []
16
16
  @start = todo.match(/^#(\s+)/)[1].size
17
17
  end
@@ -26,7 +26,7 @@ module SmartTodo
26
26
  # @param comment [String]
27
27
  # @return [void]
28
28
  def <<(comment)
29
- @comments << comment.gsub(/^#(\s+)/, '')
29
+ @comments << comment.gsub(/^#(\s+)/, "")
30
30
  end
31
31
 
32
32
  # Check if the +comment+ is indented two spaces below the
@@ -36,7 +36,7 @@ module SmartTodo
36
36
  # @param comment [String]
37
37
  # @return [true, false]
38
38
  def indented_comment?(comment)
39
- comment.match(/^#(\s+)/)[1].size - @start == DEFAULT_RUBY_INDENTATION
39
+ comment.match(/^#(\s*)/)[1].size - @start == DEFAULT_RUBY_INDENTATION
40
40
  end
41
41
  end
42
42
  end