smart_todo 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d203126f1877df2db5e717ea31690fc81f42efe1da8c3286ac619e946f6bd75
4
- data.tar.gz: cd9e9bbc4ab3a93db418d16296a9c634f55b571ad5dfe159ed8b25b6ec37cc22
3
+ metadata.gz: eed299ced3a12723ccf8cb775ea33b3ce784b1c05c18ced890b3c8b184c0124e
4
+ data.tar.gz: a2e1ff5973fb8b1b7b40ee700a56acbee6c6c30df0c23e16e8c9505fe24af8ba
5
5
  SHA512:
6
- metadata.gz: 89fc8bb08ed5ea7be2590b8bcfc38bd2e5cc75e2da96c200b70a4a7308a77b2ab2592d8190433d1d9a97e649e20f2bc6a965fb48161223e9b3d9d93e32ae4c30
7
- data.tar.gz: cc43abab984dfb7bfd1c97fdcfb0ea7bf045522ee328f46527fb15cfe899e64bd62ead2f63a9a5135f881a4ad757d84948aea23f7a89271e314241a5d8d3e9cf
6
+ metadata.gz: a36e6f20daabc1e3cdd74d8c5b63fd6284e0581de67b769dbeea5c97948f438cddefae166cf8c260d11237e6fbb22beeab9e0da1b0c069277f10c7d4ac1468ba
7
+ data.tar.gz: e020e58592618dd6567d8031dfe4c5c8c77bc7ccb372d7de2060c98f13ef072eaf31a01e16d2e9bc07118ea8553b2d716c95df19a0aed105b45baea1c6bf2406
@@ -15,7 +15,7 @@ jobs:
15
15
  version: [3.0, 3.1, 3.2, 3.3]
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v2
18
+ - uses: actions/checkout@v4
19
19
  - name: Set up Ruby ${{ matrix.version }}
20
20
  uses: ruby/setup-ruby@v1
21
21
  with:
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
 
9
9
  steps:
10
- - uses: actions/checkout@v2
10
+ - uses: actions/checkout@v4
11
11
  - name: Set up Ruby
12
12
  uses: ruby/setup-ruby@v1
13
13
  with:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_todo (1.9.1)
4
+ smart_todo (1.10.0)
5
5
  prism (~> 1.0)
6
6
 
7
7
  GEM
@@ -9,44 +9,47 @@ GEM
9
9
  specs:
10
10
  addressable (2.8.7)
11
11
  public_suffix (>= 2.0.2, < 7.0)
12
- ast (2.4.2)
12
+ ast (2.4.3)
13
13
  bigdecimal (3.1.9)
14
14
  crack (1.0.0)
15
15
  bigdecimal
16
16
  rexml
17
17
  hashdiff (1.1.2)
18
- json (2.7.1)
19
- language_server-protocol (3.17.0.3)
20
- minitest (5.25.4)
21
- parallel (1.24.0)
22
- parser (3.3.0.5)
18
+ json (2.10.2)
19
+ language_server-protocol (3.17.0.4)
20
+ lint_roller (1.1.0)
21
+ minitest (5.25.5)
22
+ parallel (1.26.3)
23
+ parser (3.3.7.3)
23
24
  ast (~> 2.4.1)
24
25
  racc
25
- prism (1.0.0)
26
+ prism (1.4.0)
26
27
  public_suffix (6.0.1)
27
- racc (1.7.3)
28
+ racc (1.8.1)
28
29
  rainbow (3.1.1)
29
- rake (13.0.6)
30
- regexp_parser (2.9.0)
31
- rexml (3.4.0)
32
- rubocop (1.62.1)
30
+ rake (13.2.1)
31
+ regexp_parser (2.10.0)
32
+ rexml (3.4.1)
33
+ rubocop (1.74.0)
33
34
  json (~> 2.3)
34
- language_server-protocol (>= 3.17.0)
35
+ language_server-protocol (~> 3.17.0.2)
36
+ lint_roller (~> 1.1.0)
35
37
  parallel (~> 1.10)
36
38
  parser (>= 3.3.0.2)
37
39
  rainbow (>= 2.2.2, < 4.0)
38
- regexp_parser (>= 1.8, < 3.0)
39
- rexml (>= 3.2.5, < 4.0)
40
- rubocop-ast (>= 1.31.1, < 2.0)
40
+ regexp_parser (>= 2.9.3, < 3.0)
41
+ rubocop-ast (>= 1.38.0, < 2.0)
41
42
  ruby-progressbar (~> 1.7)
42
- unicode-display_width (>= 2.4.0, < 3.0)
43
- rubocop-ast (1.31.2)
44
- parser (>= 3.3.0.4)
45
- rubocop-shopify (2.14.0)
43
+ unicode-display_width (>= 2.4.0, < 4.0)
44
+ rubocop-ast (1.42.0)
45
+ parser (>= 3.3.7.2)
46
+ rubocop-shopify (2.15.1)
46
47
  rubocop (~> 1.51)
47
48
  ruby-progressbar (1.13.0)
48
- unicode-display_width (2.5.0)
49
- webmock (3.24.0)
49
+ unicode-display_width (3.1.4)
50
+ unicode-emoji (~> 4.0, >= 4.0.4)
51
+ unicode-emoji (4.0.4)
52
+ webmock (3.25.1)
50
53
  addressable (>= 2.8.0)
51
54
  crack (>= 0.3.2)
52
55
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -66,6 +66,9 @@ module SmartTodo
66
66
  opts.on("--dispatcher DISPATCHER") do |dispatcher|
67
67
  @options[:dispatcher] = dispatcher
68
68
  end
69
+ opts.on("--repo [REPO]", "Repository name to include in notifications") do |repo|
70
+ @options[:repo] = repo || File.basename(Dir.pwd)
71
+ end
69
72
  end
70
73
  end
71
74
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  module SmartTodo
4
4
  class CommentParser
5
+ SUPPORTED_TAGS = ["TODO", "FIXME", "OPTIMIZE"].freeze
6
+ TAG_PATTERN = /^#\s(#{SUPPORTED_TAGS.join("|")})\(/
7
+
5
8
  attr_reader :todos
6
9
 
7
10
  def initialize
@@ -54,7 +57,7 @@ module SmartTodo
54
57
 
55
58
  source = comment.location.slice
56
59
 
57
- if source.match?(/^#\sTODO\(/)
60
+ if source.match?(TAG_PATTERN)
58
61
  todos << current_todo if current_todo
59
62
  current_todo = Todo.new(source, filepath)
60
63
  elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2)
@@ -68,7 +68,7 @@ module SmartTodo
68
68
  <<~EOM
69
69
  #{header}
70
70
 
71
- You have an assigned TODO in the `#{@file}` file.
71
+ You have an assigned TODO in the `#{@file}` file#{repo}.
72
72
  #{@event_message}
73
73
 
74
74
  Here is the associated comment on your TODO:
@@ -91,6 +91,15 @@ module SmartTodo
91
91
  def existing_user
92
92
  "Hello :wave:,"
93
93
  end
94
+
95
+ def repo
96
+ repo = @options[:repo]
97
+ return unless repo
98
+
99
+ unless repo.empty?
100
+ " in repository `#{repo}`"
101
+ end
102
+ end
94
103
  end
95
104
  end
96
105
  end
@@ -7,7 +7,8 @@ module SmartTodo
7
7
  class Slack < Base
8
8
  class << self
9
9
  def validate_options!(options)
10
- options[:slack_token] ||= ENV.fetch("SMART_TODO_SLACK_TOKEN") { raise(ArgumentError, "Missing :slack_token") }
10
+ options[:slack_token] ||= ENV.fetch("SMART_TODO_SLACK_TOKEN", "")
11
+ raise(ArgumentError, "Missing :slack_token") if options[:slack_token].empty?
11
12
 
12
13
  options.fetch(:fallback_channel) { raise(ArgumentError, "Missing :fallback_channel") }
13
14
  end
@@ -35,15 +36,17 @@ module SmartTodo
35
36
  def dispatch_one(assignee)
36
37
  user = slack_user_or_channel(assignee)
37
38
 
38
- client.post_message(user.dig("user", "id"), slack_message(user, assignee))
39
- rescue SlackClient::Error => error
40
- if ["users_not_found", "channel_not_found", "is_archived"].include?(error.error_code)
41
- user = { "user" => { "id" => @options[:fallback_channel] }, "fallback" => true }
42
- else
43
- raise(error)
44
- end
39
+ return unless user
45
40
 
46
- client.post_message(user.dig("user", "id"), slack_message(user, assignee))
41
+ begin
42
+ client.post_message(user.dig("user", "id"), slack_message(user, assignee))
43
+ rescue SlackClient::Error => error
44
+ user = handle_slack_error(error, "Error dispatching message")
45
+ retry
46
+ rescue Net::HTTPError => error
47
+ $stderr.puts "Error dispatching message: #{error.message}"
48
+ $stderr.puts "Response: #{error.response.body}"
49
+ end
47
50
  end
48
51
 
49
52
  private
@@ -58,12 +61,25 @@ module SmartTodo
58
61
  else
59
62
  { "user" => { "id" => assignee } }
60
63
  end
64
+ rescue SlackClient::Error => error
65
+ handle_slack_error(error, "Error finding user or channel")
66
+ rescue Net::HTTPError => error
67
+ $stderr.puts "Error finding user or channel: #{error.message}"
68
+ $stderr.puts "Response: #{error.response.body}"
61
69
  end
62
70
 
63
71
  # @return [SlackClient] an instance of SlackClient
64
72
  def client
65
73
  @client ||= SlackClient.new(@options[:slack_token])
66
74
  end
75
+
76
+ def handle_slack_error(error, message)
77
+ if ["users_not_found", "channel_not_found", "is_archived"].include?(error.error_code)
78
+ { "user" => { "id" => @options[:fallback_channel] }, "fallback" => true }
79
+ else
80
+ $stderr.puts "#{message}: #{error.message}"
81
+ end
82
+ end
67
83
  end
68
84
  end
69
85
  end
@@ -32,16 +32,17 @@ module SmartTodo
32
32
  # Retrieve the Slack ID of a user from his email
33
33
  #
34
34
  # @param email [String]
35
+ # @param min_sleep [Integer] - the minimum sleep time in seconds in case of rate limiting
35
36
  # @return [Hash]
36
37
  #
37
38
  # @raise [Net::HTTPError] in case the request to Slack failed
38
39
  # @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
39
40
  #
40
41
  # @see https://api.slack.com/methods/users.lookupByEmail
41
- def lookup_user_by_email(email)
42
+ def lookup_user_by_email(email, min_sleep = 30)
42
43
  headers = default_headers.merge("Content-Type" => "application/x-www-form-urlencoded")
43
44
  request = Net::HTTP::Get.new("/api/users.lookupByEmail?email=#{CGI.escape(email)}", headers)
44
- dispatch(request)
45
+ dispatch(request, 5, min_sleep)
45
46
  end
46
47
 
47
48
  # Send a message to a Slack channel or to a user
@@ -63,25 +64,27 @@ module SmartTodo
63
64
 
64
65
  private
65
66
 
66
- # @param method [Symbol]
67
- # @param endpoint [String]
68
- # @param data [String] JSON encoded data when making a POST request
69
- # @param headers [Hash]
67
+ # @param request [Net::HTTPRequest]
68
+ # @param max_attempts [Integer] - the maximum number of attempts to make
69
+ # @param min_sleep [Integer] - the minimum sleep time in seconds in case of rate limiting
70
70
  #
71
71
  # @raise [Net::HTTPError] in case the request to Slack failed
72
72
  # @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
73
- def dispatch(request, max_attempts = 5)
73
+ def dispatch(request, max_attempts = 5, min_sleep = 30)
74
74
  response = @client.request(request)
75
75
  attempts = 1
76
76
 
77
77
  while response.is_a?(Net::HTTPTooManyRequests) && attempts < max_attempts
78
- sleep([Integer(response["Retry-After"]), 600].min)
78
+ sleep_time = Integer(response["Retry-After"]).clamp(min_sleep, 120)
79
+ puts "Rate limited, sleeping for #{sleep_time} seconds"
80
+ sleep(sleep_time)
79
81
  response = @client.request(request)
80
82
  attempts += 1
81
83
  end
82
84
 
83
85
  unless response.code_type < Net::HTTPSuccess
84
- raise(Net::HTTPError.new("Request to slack failed", response))
86
+ message = "Request to slack failed #{response.body}, code #{response.code}, attempt #{attempts}"
87
+ raise(Net::HTTPError.new(message, response))
85
88
  end
86
89
 
87
90
  body = JSON.parse(response.body)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmartTodo
4
- VERSION = "1.9.1"
4
+ VERSION = "1.10.0"
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "smart_todo"
4
+ require "date"
4
5
 
5
6
  module RuboCop
6
7
  module Cop
@@ -12,12 +13,20 @@ module RuboCop
12
13
  class SmartTodoCop < Base
13
14
  HELP = "For more info please look at https://github.com/Shopify/smart_todo/wiki/Syntax"
14
15
  MSG = "Don't write regular TODO comments. Write SmartTodo compatible syntax comments. #{HELP}"
16
+ INVESTIGATED_TAGS = ::SmartTodo::CommentParser::SUPPORTED_TAGS +
17
+ ::SmartTodo::CommentParser::SUPPORTED_TAGS.map(&:downcase)
18
+ TODO_PATTERN = /^#\s@?(#{INVESTIGATED_TAGS.join("|")})\b/
15
19
 
16
20
  # @param processed_source [RuboCop::ProcessedSource]
17
21
  # @return [void]
18
22
  def on_new_investigation
19
23
  processed_source.comments.each do |comment|
20
- next unless /^#\sTODO/.match?(comment.text)
24
+ next unless (match = TODO_PATTERN.match(comment.text))
25
+
26
+ if match[1] != match[1].upcase
27
+ add_offense(comment)
28
+ next
29
+ end
21
30
 
22
31
  metadata = metadata(comment.text)
23
32
 
@@ -25,8 +34,10 @@ module RuboCop
25
34
  add_offense(comment, message: "Invalid TODO format: #{metadata.errors.join(", ")}. #{HELP}")
26
35
  elsif !smart_todo?(metadata)
27
36
  add_offense(comment)
28
- elsif (methods = invalid_event_methods(metadata.events)).any?
29
- add_offense(comment, message: "Invalid event method(s): #{methods.join(", ")}. #{HELP}")
37
+ elsif invalid_assignees(metadata.assignees).any?
38
+ add_offense(comment, message: "Invalid event assignee. This method only accepts strings. #{HELP}")
39
+ elsif (invalid_events = validate_events(metadata.events)).any?
40
+ add_offense(comment, message: "#{invalid_events.join(". ")}. #{HELP}")
30
41
  end
31
42
  end
32
43
  end
@@ -47,10 +58,93 @@ module RuboCop
47
58
  metadata.assignees.any?
48
59
  end
49
60
 
50
- # @param metadata [Array<SmartTodo::Parser::MethodNode>]
61
+ # @param assignees [Array]
62
+ # @return [Array]
63
+ def invalid_assignees(assignees)
64
+ assignees.reject { |assignee| assignee.is_a?(String) }
65
+ end
66
+
67
+ # @param events [Array<SmartTodo::Todo::CallNode>]
51
68
  # @return [Array<String>]
52
- def invalid_event_methods(events)
53
- events.map(&:method_name).reject { |method| ::SmartTodo::Events.method_defined?(method) }
69
+ def validate_events(events)
70
+ invalid_methods = events.map(&:method_name).reject { |method| ::SmartTodo::Events.method_defined?(method) }
71
+ return ["Invalid event method(s): #{invalid_methods.join(", ")}"] if invalid_methods.any?
72
+
73
+ events.map do |event|
74
+ send(validate_method(event.method_name), event.arguments)
75
+ end.compact
76
+ end
77
+
78
+ # @param event_type [Symbol]
79
+ # @return [String]
80
+ def validate_method(event_type)
81
+ "validate_#{event_type}_args"
82
+ end
83
+
84
+ # @param args [Array]
85
+ # @return [String, nil] Returns error message if date is invalid, nil if valid
86
+ def validate_date_args(args)
87
+ date = args.first
88
+ Date.parse(date)
89
+ nil
90
+ rescue ArgumentError, TypeError
91
+ "Invalid date format: #{date}"
92
+ end
93
+
94
+ # @param args [Array]
95
+ # @return [String, nil] Returns error message if arguments are invalid, nil if valid
96
+ def validate_issue_close_args(args)
97
+ validate_fixed_arity_args(args, 3, "issue_close", ["organization", "repo", "issue_number"])
98
+ end
99
+
100
+ # @param args [Array]
101
+ # @return [String, nil] Returns error message if arguments are invalid, nil if valid
102
+ def validate_pull_request_close_args(args)
103
+ validate_fixed_arity_args(args, 3, "pull_request_close", ["organization", "repo", "pr_number"])
104
+ end
105
+
106
+ # @param args [Array]
107
+ # @return [String, nil] Returns error message if arguments are invalid, nil if valid
108
+ def validate_gem_release_args(args)
109
+ validate_gem_args(args, "gem_release")
110
+ end
111
+
112
+ # @param args [Array]
113
+ # @return [String, nil] Returns error message if arguments are invalid, nil if valid
114
+ def validate_gem_bump_args(args)
115
+ validate_gem_args(args, "gem_bump")
116
+ end
117
+
118
+ # @param args [Array]
119
+ # @return [String, nil] Returns error message if arguments are invalid, nil if valid
120
+ def validate_ruby_version_args(args)
121
+ if args.empty?
122
+ "Invalid ruby_version event: Expected at least 1 argument (version requirement), got 0"
123
+ elsif !args.all? { |arg| arg.is_a?(String) }
124
+ "Invalid ruby_version event: Version requirements must be strings"
125
+ end
126
+ end
127
+
128
+ # Helper method for validating fixed arity events
129
+ def validate_fixed_arity_args(args, expected_count, event_name, arg_names)
130
+ if args.size != expected_count
131
+ message = "Invalid #{event_name} event: Expected #{expected_count} arguments "
132
+ message += "(#{arg_names.join(", ")}), got #{args.size}"
133
+ message
134
+ elsif !args.all? { |arg| arg.is_a?(String) }
135
+ "Invalid #{event_name} event: Arguments must be strings"
136
+ end
137
+ end
138
+
139
+ # Helper method for validating gem-related events
140
+ def validate_gem_args(args, event_name)
141
+ if args.empty?
142
+ "Invalid #{event_name} event: Expected at least 1 argument (gem_name), got 0"
143
+ elsif !args[0].is_a?(String)
144
+ "Invalid #{event_name} event: First argument (gem_name) must be a string"
145
+ elsif args.size > 1 && !args[1..].all? { |arg| arg.is_a?(String) }
146
+ "Invalid #{event_name} event: Version requirements must be strings"
147
+ end
54
148
  end
55
149
  end
56
150
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_todo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-04 00:00:00.000000000 Z
10
+ date: 2025-04-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: prism
@@ -147,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubygems_version: 3.6.3
150
+ rubygems_version: 3.6.6
151
151
  specification_version: 4
152
152
  summary: Enhance todo's comments in your codebase.
153
153
  test_files: []