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 +4 -4
- data/.github/workflows/build.yml +1 -1
- data/.github/workflows/rubocop.yml +1 -1
- data/Gemfile.lock +26 -23
- data/lib/smart_todo/cli.rb +3 -0
- data/lib/smart_todo/comment_parser.rb +4 -1
- data/lib/smart_todo/dispatchers/base.rb +10 -1
- data/lib/smart_todo/dispatchers/slack.rb +25 -9
- data/lib/smart_todo/slack_client.rb +12 -9
- data/lib/smart_todo/version.rb +1 -1
- data/lib/smart_todo_cop.rb +100 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eed299ced3a12723ccf8cb775ea33b3ce784b1c05c18ced890b3c8b184c0124e
|
4
|
+
data.tar.gz: a2e1ff5973fb8b1b7b40ee700a56acbee6c6c30df0c23e16e8c9505fe24af8ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a36e6f20daabc1e3cdd74d8c5b63fd6284e0581de67b769dbeea5c97948f438cddefae166cf8c260d11237e6fbb22beeab9e0da1b0c069277f10c7d4ac1468ba
|
7
|
+
data.tar.gz: e020e58592618dd6567d8031dfe4c5c8c77bc7ccb372d7de2060c98f13ef072eaf31a01e16d2e9bc07118ea8553b2d716c95df19a0aed105b45baea1c6bf2406
|
data/.github/workflows/build.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
smart_todo (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.
|
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.
|
19
|
-
language_server-protocol (3.17.0.
|
20
|
-
|
21
|
-
|
22
|
-
|
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.
|
26
|
+
prism (1.4.0)
|
26
27
|
public_suffix (6.0.1)
|
27
|
-
racc (1.
|
28
|
+
racc (1.8.1)
|
28
29
|
rainbow (3.1.1)
|
29
|
-
rake (13.
|
30
|
-
regexp_parser (2.
|
31
|
-
rexml (3.4.
|
32
|
-
rubocop (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 (
|
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 (>=
|
39
|
-
|
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, <
|
43
|
-
rubocop-ast (1.
|
44
|
-
parser (>= 3.3.
|
45
|
-
rubocop-shopify (2.
|
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 (
|
49
|
-
|
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)
|
data/lib/smart_todo/cli.rb
CHANGED
@@ -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?(
|
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"
|
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
|
-
|
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
|
-
|
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
|
67
|
-
# @param
|
68
|
-
# @param
|
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
|
-
|
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
|
-
|
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)
|
data/lib/smart_todo/version.rb
CHANGED
data/lib/smart_todo_cop.rb
CHANGED
@@ -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
|
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 (
|
29
|
-
add_offense(comment, message: "Invalid event method
|
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
|
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
|
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.
|
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-
|
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.
|
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: []
|