smart_todo 1.5.0 → 1.7.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/Gemfile.lock +3 -2
- data/bin/profile +16 -0
- data/lib/smart_todo/cli.rb +46 -12
- data/lib/smart_todo/comment_parser.rb +71 -0
- data/lib/smart_todo/dispatchers/base.rb +1 -1
- data/lib/smart_todo/events.rb +162 -10
- data/lib/smart_todo/http_client_builder.rb +19 -0
- data/lib/smart_todo/slack_client.rb +1 -5
- data/lib/smart_todo/todo.rb +84 -0
- data/lib/smart_todo/version.rb +1 -1
- data/lib/smart_todo.rb +4 -13
- data/lib/smart_todo_cop.rb +4 -4
- data/smart_todo.gemspec +1 -1
- metadata +12 -15
- data/lib/smart_todo/events/date.rb +0 -28
- data/lib/smart_todo/events/gem_bump.rb +0 -57
- data/lib/smart_todo/events/gem_release.rb +0 -69
- data/lib/smart_todo/events/issue_close.rb +0 -86
- data/lib/smart_todo/parser/comment_parser.rb +0 -71
- data/lib/smart_todo/parser/metadata_parser.rb +0 -124
- data/lib/smart_todo/parser/todo_node.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bbca316fba93242bc5ea22479579407ed9ce3ed93a6bd618978223f96f0626d
|
4
|
+
data.tar.gz: c91cf35475f5a32da647f6ed649ac7d50d85a929908aa29f1768a9b1d6db75da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1668111d5578d6c64f2ff7df6071f9ede6ccc4aeb01bf1c5cdccea8d514cf29caad0b2b576bb3bb60eaaaa309f44b723e066c62b39b50ab17e418372a29c4dad
|
7
|
+
data.tar.gz: 25636e700c032b2bc0509791f8884f9e9f6cb78ae09103db503d69739e48507c07c8a8fc6579f4f441c1f08caf09f19274ad19061305b9ad9b82efc5c41e9711
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
smart_todo (1.
|
5
|
-
|
4
|
+
smart_todo (1.7.0)
|
5
|
+
prism (~> 0.15)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
@@ -19,6 +19,7 @@ GEM
|
|
19
19
|
parser (3.2.2.3)
|
20
20
|
ast (~> 2.4.1)
|
21
21
|
racc
|
22
|
+
prism (0.17.0)
|
22
23
|
public_suffix (4.0.6)
|
23
24
|
racc (1.7.0)
|
24
25
|
rainbow (3.1.1)
|
data/bin/profile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "smart_todo"
|
6
|
+
|
7
|
+
class NullDispatcher < SmartTodo::Dispatchers::Base
|
8
|
+
class << self
|
9
|
+
def validate_options!(_); end
|
10
|
+
end
|
11
|
+
|
12
|
+
def dispatch
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
exit SmartTodo::CLI.new(NullDispatcher).run
|
data/lib/smart_todo/cli.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "optionparser"
|
4
|
+
require "etc"
|
4
5
|
|
5
6
|
module SmartTodo
|
6
7
|
# This class is the entrypoint of the SmartTodo library and is responsible
|
7
8
|
# to retrieve the command line options as well as iterating over each files/directories
|
8
9
|
# to run the +CommentParser+ on.
|
9
10
|
class CLI
|
10
|
-
def initialize
|
11
|
+
def initialize(dispatcher = nil)
|
11
12
|
@options = {}
|
12
13
|
@errors = []
|
14
|
+
@dispatcher = dispatcher
|
13
15
|
end
|
14
16
|
|
15
17
|
# @param args [Array<String>]
|
@@ -19,15 +21,18 @@ module SmartTodo
|
|
19
21
|
|
20
22
|
paths << "." if paths.empty?
|
21
23
|
|
24
|
+
comment_parser = CommentParser.new
|
22
25
|
paths.each do |path|
|
23
|
-
normalize_path(path).each do |
|
24
|
-
parse_file(
|
26
|
+
normalize_path(path).each do |filepath|
|
27
|
+
comment_parser.parse_file(filepath)
|
25
28
|
|
26
29
|
$stdout.print(".")
|
27
30
|
$stdout.flush
|
28
31
|
end
|
29
32
|
end
|
30
33
|
|
34
|
+
process_dispatches(process_todos(comment_parser.todos))
|
35
|
+
|
31
36
|
if @errors.empty?
|
32
37
|
0
|
33
38
|
else
|
@@ -79,14 +84,17 @@ module SmartTodo
|
|
79
84
|
end
|
80
85
|
end
|
81
86
|
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
def process_todos(todos)
|
88
|
+
events = Events.new
|
89
|
+
dispatches = []
|
90
|
+
|
91
|
+
todos.each do |todo|
|
85
92
|
event_message = nil
|
86
|
-
event_met =
|
87
|
-
event_message =
|
93
|
+
event_met = todo.events.find do |event|
|
94
|
+
event_message = events.public_send(event.method_name, *event.arguments)
|
88
95
|
rescue => e
|
89
|
-
message = "Error while parsing #{
|
96
|
+
message = "Error while parsing #{todo.filepath} on event `#{event.method_name}` " \
|
97
|
+
"with arguments #{event.arguments.map(&:inspect)}: " \
|
90
98
|
"#{e.message}"
|
91
99
|
|
92
100
|
@errors << message
|
@@ -94,10 +102,36 @@ module SmartTodo
|
|
94
102
|
nil
|
95
103
|
end
|
96
104
|
|
97
|
-
@errors.concat(
|
98
|
-
|
99
|
-
dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
|
105
|
+
@errors.concat(todo.errors)
|
106
|
+
dispatches << [event_message, todo] if event_met
|
100
107
|
end
|
108
|
+
|
109
|
+
dispatches
|
110
|
+
end
|
111
|
+
|
112
|
+
def process_dispatches(dispatches)
|
113
|
+
queue = Queue.new
|
114
|
+
dispatches.each { |dispatch| queue << dispatch }
|
115
|
+
|
116
|
+
thread_count = Etc.nprocessors
|
117
|
+
thread_count.times { queue << nil }
|
118
|
+
|
119
|
+
threads =
|
120
|
+
thread_count.times.map do
|
121
|
+
Thread.new do
|
122
|
+
Thread.current.abort_on_exception = true
|
123
|
+
|
124
|
+
loop do
|
125
|
+
dispatch = queue.pop
|
126
|
+
break if dispatch.nil?
|
127
|
+
|
128
|
+
(event_message, todo) = dispatch
|
129
|
+
dispatcher.new(event_message, todo, todo.filepath, @options).dispatch
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
threads.each(&:join)
|
101
135
|
end
|
102
136
|
end
|
103
137
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
class CommentParser
|
5
|
+
attr_reader :todos
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@todos = []
|
9
|
+
end
|
10
|
+
|
11
|
+
if Prism.respond_to?(:parse_comments)
|
12
|
+
def parse(source, filepath = "-e")
|
13
|
+
parse_comments(Prism.parse_comments(source), filepath)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_file(filepath)
|
17
|
+
parse_comments(Prism.parse_file_comments(filepath), filepath)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
def parse(source, filepath = "-e")
|
21
|
+
parse_comments(Prism.parse(source, filepath).comments, filepath)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_file(filepath)
|
25
|
+
parse_comments(Prism.parse_file(filepath).comments, filepath)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def parse(source)
|
31
|
+
parser = new
|
32
|
+
parser.parse(source)
|
33
|
+
parser.todos
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
if defined?(Prism::InlineComment)
|
40
|
+
def inline?(comment)
|
41
|
+
comment.is_a?(Prism::InlineComment)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
def inline?(comment)
|
45
|
+
comment.type == :inline
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_comments(comments, filepath)
|
50
|
+
current_todo = nil
|
51
|
+
|
52
|
+
comments.each do |comment|
|
53
|
+
next unless inline?(comment)
|
54
|
+
|
55
|
+
source = comment.location.slice
|
56
|
+
|
57
|
+
if source.match?(/^#\sTODO\(/)
|
58
|
+
todos << current_todo if current_todo
|
59
|
+
current_todo = Todo.new(source, filepath)
|
60
|
+
elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2)
|
61
|
+
current_todo << "#{source[(indent + 1)..]}\n"
|
62
|
+
else
|
63
|
+
todos << current_todo if current_todo
|
64
|
+
current_todo = nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
todos << current_todo if current_todo
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -40,7 +40,7 @@ module SmartTodo
|
|
40
40
|
@todo_node = todo_node
|
41
41
|
@options = options
|
42
42
|
@file = file
|
43
|
-
@assignees = @todo_node.
|
43
|
+
@assignees = @todo_node.assignees
|
44
44
|
end
|
45
45
|
|
46
46
|
# This method gets called when a TODO reminder is expired and needs to be delivered.
|
data/lib/smart_todo/events.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
gem("bundler")
|
4
|
+
require "bundler"
|
5
|
+
require "net/http"
|
6
|
+
require "time"
|
7
|
+
require "json"
|
8
|
+
|
3
9
|
module SmartTodo
|
4
10
|
# This module contains all the methods accessible for SmartTodo comments.
|
5
11
|
# It is meant to be reopened by the host application in order to define
|
@@ -10,7 +16,7 @@ module SmartTodo
|
|
10
16
|
#
|
11
17
|
# @example Adding a custom event
|
12
18
|
# module SmartTodo
|
13
|
-
#
|
19
|
+
# class Events
|
14
20
|
# def trello_card_close(card)
|
15
21
|
# ...
|
16
22
|
# end
|
@@ -18,33 +24,87 @@ module SmartTodo
|
|
18
24
|
# end
|
19
25
|
#
|
20
26
|
# TODO(on: trello_card_close(381), to: 'john@example.com')
|
21
|
-
|
22
|
-
|
27
|
+
#
|
28
|
+
class Events
|
29
|
+
def initialize(now: nil, spec_set: nil, current_ruby_version: nil)
|
30
|
+
@now = now
|
31
|
+
@spec_set = spec_set
|
32
|
+
@rubygems_client = nil
|
33
|
+
@github_client = nil
|
34
|
+
@current_ruby_version = current_ruby_version
|
35
|
+
end
|
23
36
|
|
24
37
|
# Check if the +date+ is in the past
|
25
38
|
#
|
26
|
-
# @param
|
39
|
+
# @param on_date [String] a string parsable by Time.parse
|
27
40
|
# @return [false, String]
|
28
|
-
def date(
|
29
|
-
|
41
|
+
def date(on_date)
|
42
|
+
if now >= Time.parse(on_date)
|
43
|
+
"We are past the *#{on_date}* due date and " \
|
44
|
+
"your TODO is now ready to be addressed."
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
30
48
|
end
|
31
49
|
|
32
50
|
# Check if a new version of +gem_name+ was released with the +requirements+ expected
|
33
51
|
#
|
52
|
+
# @example Expecting a specific version
|
53
|
+
# gem_release('rails', '6.0')
|
54
|
+
#
|
55
|
+
# @example Expecting a version in the 5.x.x series
|
56
|
+
# gem_release('rails', '> 5.2', '< 6')
|
57
|
+
#
|
34
58
|
# @param gem_name [String]
|
35
59
|
# @param requirements [Array<String>] a list of version specifiers
|
36
60
|
# @return [false, String]
|
37
61
|
def gem_release(gem_name, *requirements)
|
38
|
-
|
62
|
+
response = rubygems_client.get("/api/v1/versions/#{gem_name}.json")
|
63
|
+
|
64
|
+
if response.code_type < Net::HTTPClientError
|
65
|
+
"The gem *#{gem_name}* doesn't seem to exist, I can't determine if " \
|
66
|
+
"your TODO is ready to be addressed."
|
67
|
+
else
|
68
|
+
requirement = Gem::Requirement.new(requirements)
|
69
|
+
version = JSON.parse(response.body).find { |gem| requirement.satisfied_by?(Gem::Version.new(gem["number"])) }
|
70
|
+
|
71
|
+
if version
|
72
|
+
"The gem *#{gem_name}* was released to version *#{version["number"]}* and " \
|
73
|
+
"your TODO is now ready to be addressed."
|
74
|
+
else
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
39
78
|
end
|
40
79
|
|
41
80
|
# Check if +gem_name+ was bumped to the +requirements+ expected
|
42
81
|
#
|
82
|
+
# @example Expecting a specific version
|
83
|
+
# gem_bump('rails', '6.0')
|
84
|
+
#
|
85
|
+
# @example Expecting a version in the 5.x.x series
|
86
|
+
# gem_bump('rails', '> 5.2', '< 6')
|
87
|
+
#
|
43
88
|
# @param gem_name [String]
|
44
89
|
# @param requirements [Array<String>] a list of version specifiers
|
45
90
|
# @return [false, String]
|
46
91
|
def gem_bump(gem_name, *requirements)
|
47
|
-
|
92
|
+
specs = spec_set[gem_name]
|
93
|
+
|
94
|
+
if specs.empty?
|
95
|
+
"The gem *#{gem_name}* is not in your dependencies, I can't determine if " \
|
96
|
+
"your TODO is ready to be addressed."
|
97
|
+
else
|
98
|
+
requirement = Gem::Requirement.new(requirements)
|
99
|
+
version = specs.first.version
|
100
|
+
|
101
|
+
if requirement.satisfied_by?(version)
|
102
|
+
"The gem *#{gem_name}* was updated to version *#{version}* and " \
|
103
|
+
"your TODO is now ready to be addressed."
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
48
108
|
end
|
49
109
|
|
50
110
|
# Check if the issue +issue_number+ is closed
|
@@ -54,7 +114,22 @@ module SmartTodo
|
|
54
114
|
# @param issue_number [String, Integer]
|
55
115
|
# @return [false, String]
|
56
116
|
def issue_close(organization, repo, issue_number)
|
57
|
-
|
117
|
+
headers = github_headers(organization, repo)
|
118
|
+
response = github_client.get("/repos/#{organization}/#{repo}/issues/#{issue_number}", headers)
|
119
|
+
|
120
|
+
if response.code_type < Net::HTTPClientError
|
121
|
+
<<~EOM
|
122
|
+
I can't retrieve the information from the issue *#{issue_number}* in the *#{organization}/#{repo}* repository.
|
123
|
+
|
124
|
+
If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
|
125
|
+
environment variable with a correct GitHub token.
|
126
|
+
EOM
|
127
|
+
elsif JSON.parse(response.body)["state"] == "closed"
|
128
|
+
"The issue https://github.com/#{organization}/#{repo}/issues/#{issue_number} is now closed, " \
|
129
|
+
"your TODO is ready to be addressed."
|
130
|
+
else
|
131
|
+
false
|
132
|
+
end
|
58
133
|
end
|
59
134
|
|
60
135
|
# Check if the pull request +pr_number+ is closed
|
@@ -64,7 +139,84 @@ module SmartTodo
|
|
64
139
|
# @param pr_number [String, Integer]
|
65
140
|
# @return [false, String]
|
66
141
|
def pull_request_close(organization, repo, pr_number)
|
67
|
-
|
142
|
+
headers = github_headers(organization, repo)
|
143
|
+
response = github_client.get("/repos/#{organization}/#{repo}/pulls/#{pr_number}", headers)
|
144
|
+
|
145
|
+
if response.code_type < Net::HTTPClientError
|
146
|
+
<<~EOM
|
147
|
+
I can't retrieve the information from the PR *#{pr_number}* in the *#{organization}/#{repo}* repository.
|
148
|
+
|
149
|
+
If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
|
150
|
+
environment variable with a correct GitHub token.
|
151
|
+
EOM
|
152
|
+
elsif JSON.parse(response.body)["state"] == "closed"
|
153
|
+
"The pull request https://github.com/#{organization}/#{repo}/pull/#{pr_number} is now closed, " \
|
154
|
+
"your TODO is ready to be addressed."
|
155
|
+
else
|
156
|
+
false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Check if the installed ruby version meets requirements.
|
161
|
+
#
|
162
|
+
# @param requirements [Array<String>] a list of version specifiers
|
163
|
+
# @return [false, String]
|
164
|
+
def ruby_version(*requirements)
|
165
|
+
requirement = Gem::Requirement.new(requirements)
|
166
|
+
|
167
|
+
if requirement.satisfied_by?(current_ruby_version)
|
168
|
+
"The currently installed version of Ruby #{current_ruby_version} is #{requirement}."
|
169
|
+
else
|
170
|
+
false
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def now
|
177
|
+
@now ||= Time.now
|
178
|
+
end
|
179
|
+
|
180
|
+
def spec_set
|
181
|
+
@spec_set ||= Bundler.load.specs
|
182
|
+
end
|
183
|
+
|
184
|
+
def rubygems_client
|
185
|
+
@rubygems_client ||= HttpClientBuilder.build("rubygems.org")
|
186
|
+
end
|
187
|
+
|
188
|
+
def github_client
|
189
|
+
@github_client ||= HttpClientBuilder.build("api.github.com")
|
190
|
+
end
|
191
|
+
|
192
|
+
def github_headers(organization, repo)
|
193
|
+
headers = { "Accept" => "application/vnd.github.v3+json" }
|
194
|
+
|
195
|
+
token = github_authorization_token(organization, repo)
|
196
|
+
headers["Authorization"] = "token #{token}" if token
|
197
|
+
|
198
|
+
headers
|
199
|
+
end
|
200
|
+
|
201
|
+
GITHUB_TOKEN = "SMART_TODO_GITHUB_TOKEN"
|
202
|
+
|
203
|
+
# @return [String, nil]
|
204
|
+
def github_authorization_token(organization, repo)
|
205
|
+
organization_name = organization.upcase.gsub(/[^A-Z0-9]/, "_")
|
206
|
+
repo_name = repo.upcase.gsub(/[^A-Z0-9]/, "_")
|
207
|
+
|
208
|
+
[
|
209
|
+
"#{GITHUB_TOKEN}__#{organization_name}__#{repo_name}",
|
210
|
+
"#{GITHUB_TOKEN}__#{organization_name}",
|
211
|
+
GITHUB_TOKEN,
|
212
|
+
].find do |key|
|
213
|
+
token = ENV[key]
|
214
|
+
break token unless token.nil? || token.empty?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def current_ruby_version
|
219
|
+
@current_ruby_version ||= Gem::Version.new(RUBY_VERSION)
|
68
220
|
end
|
69
221
|
end
|
70
222
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
|
5
|
+
module SmartTodo
|
6
|
+
# @api private
|
7
|
+
class HttpClientBuilder
|
8
|
+
class << self
|
9
|
+
def build(endpoint)
|
10
|
+
Net::HTTP.new(endpoint, Net::HTTP.https_default_port).tap do |client|
|
11
|
+
client.use_ssl = true
|
12
|
+
client.read_timeout = 30
|
13
|
+
client.ssl_timeout = 15
|
14
|
+
client.max_retries = 2
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -26,11 +26,7 @@ module SmartTodo
|
|
26
26
|
# @param slack_token [String]
|
27
27
|
def initialize(slack_token)
|
28
28
|
@slack_token = slack_token
|
29
|
-
@client =
|
30
|
-
client.use_ssl = true
|
31
|
-
client.read_timeout = 30
|
32
|
-
client.ssl_timeout = 15
|
33
|
-
end
|
29
|
+
@client = HttpClientBuilder.build("slack.com")
|
34
30
|
end
|
35
31
|
|
36
32
|
# Retrieve the Slack ID of a user from his email
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
class Todo
|
5
|
+
attr_reader :filepath, :comment, :indent
|
6
|
+
attr_reader :events, :assignees, :errors
|
7
|
+
|
8
|
+
def initialize(source, filepath = "-e")
|
9
|
+
@filepath = filepath
|
10
|
+
@comment = +""
|
11
|
+
@indent = source[/^#(\s+)/, 1].length
|
12
|
+
|
13
|
+
@events = []
|
14
|
+
@assignees = []
|
15
|
+
@errors = []
|
16
|
+
|
17
|
+
parse(source[(indent + 1)..])
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(source)
|
21
|
+
comment << source
|
22
|
+
end
|
23
|
+
|
24
|
+
class CallNode
|
25
|
+
attr_reader :method_name, :arguments, :location
|
26
|
+
|
27
|
+
def initialize(method_name, arguments, location)
|
28
|
+
@arguments = arguments
|
29
|
+
@method_name = method_name
|
30
|
+
@location = location
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Compiler < Prism::Compiler
|
35
|
+
attr_reader :metadata
|
36
|
+
|
37
|
+
def initialize(metadata)
|
38
|
+
super()
|
39
|
+
@metadata = metadata
|
40
|
+
end
|
41
|
+
|
42
|
+
def visit_call_node(node)
|
43
|
+
CallNode.new(node.name, visit_all(node.arguments&.arguments || []), node.location)
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_integer_node(node)
|
47
|
+
node.value
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_keyword_hash_node(node)
|
51
|
+
node.elements.each do |element|
|
52
|
+
next unless (key = element.key).is_a?(Prism::SymbolNode)
|
53
|
+
|
54
|
+
case key.unescaped.to_sym
|
55
|
+
when :on
|
56
|
+
value = visit(element.value)
|
57
|
+
|
58
|
+
if value.is_a?(CallNode)
|
59
|
+
if value.arguments.all? { |arg| arg.is_a?(Integer) || arg.is_a?(String) }
|
60
|
+
metadata.events << value
|
61
|
+
else
|
62
|
+
metadata.errors << "Incorrect `:on` event format: #{value.location.slice}"
|
63
|
+
end
|
64
|
+
else
|
65
|
+
metadata.errors << "Incorrect `:on` event format: #{value.inspect}"
|
66
|
+
end
|
67
|
+
when :to
|
68
|
+
metadata.assignees << visit(element.value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_string_node(node)
|
74
|
+
node.unescaped
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def parse(source)
|
81
|
+
Prism.parse(source).value.statements.body.first.accept(Compiler.new(self))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/smart_todo/version.rb
CHANGED
data/lib/smart_todo.rb
CHANGED
@@ -1,24 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "prism"
|
3
4
|
require "smart_todo/version"
|
4
5
|
require "smart_todo/events"
|
5
6
|
|
6
7
|
module SmartTodo
|
7
8
|
autoload :SlackClient, "smart_todo/slack_client"
|
8
9
|
autoload :CLI, "smart_todo/cli"
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
autoload :TodoNode, "smart_todo/parser/todo_node"
|
13
|
-
autoload :MetadataParser, "smart_todo/parser/metadata_parser"
|
14
|
-
end
|
15
|
-
|
16
|
-
module Events
|
17
|
-
autoload :Date, "smart_todo/events/date"
|
18
|
-
autoload :GemBump, "smart_todo/events/gem_bump"
|
19
|
-
autoload :GemRelease, "smart_todo/events/gem_release"
|
20
|
-
autoload :IssueClose, "smart_todo/events/issue_close"
|
21
|
-
end
|
10
|
+
autoload :Todo, "smart_todo/todo"
|
11
|
+
autoload :CommentParser, "smart_todo/comment_parser"
|
12
|
+
autoload :HttpClientBuilder, "smart_todo/http_client_builder"
|
22
13
|
|
23
14
|
module Dispatchers
|
24
15
|
autoload :Base, "smart_todo/dispatchers/base"
|
data/lib/smart_todo_cop.rb
CHANGED
@@ -17,7 +17,7 @@ module RuboCop
|
|
17
17
|
# @return [void]
|
18
18
|
def investigate(processed_source)
|
19
19
|
processed_source.comments.each do |comment|
|
20
|
-
next unless /^#\sTODO
|
20
|
+
next unless /^#\sTODO/.match?(comment.text)
|
21
21
|
|
22
22
|
metadata = metadata(comment.text)
|
23
23
|
|
@@ -36,21 +36,21 @@ module RuboCop
|
|
36
36
|
# @param comment [String]
|
37
37
|
# @return [SmartTodo::Parser::Visitor]
|
38
38
|
def metadata(comment)
|
39
|
-
::SmartTodo::
|
39
|
+
::SmartTodo::Todo.new(comment)
|
40
40
|
end
|
41
41
|
|
42
42
|
# @param metadata [SmartTodo::Parser::Visitor]
|
43
43
|
# @return [true, false]
|
44
44
|
def smart_todo?(metadata)
|
45
45
|
metadata.events.any? &&
|
46
|
-
metadata.events.all? { |event| event.is_a?(::SmartTodo::
|
46
|
+
metadata.events.all? { |event| event.is_a?(::SmartTodo::Todo::CallNode) } &&
|
47
47
|
metadata.assignees.any?
|
48
48
|
end
|
49
49
|
|
50
50
|
# @param metadata [Array<SmartTodo::Parser::MethodNode>]
|
51
51
|
# @return [Array<String>]
|
52
52
|
def invalid_event_methods(events)
|
53
|
-
events.map(&:method_name).reject { |method| ::SmartTodo::Events.
|
53
|
+
events.map(&:method_name).reject { |method| ::SmartTodo::Events.method_defined?(method) }
|
54
54
|
end
|
55
55
|
end
|
56
56
|
end
|
data/smart_todo.gemspec
CHANGED
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.executables = ["smart_todo"]
|
34
34
|
spec.require_paths = ["lib"]
|
35
35
|
|
36
|
-
spec.add_runtime_dependency("
|
36
|
+
spec.add_runtime_dependency("prism", "~> 0.15")
|
37
37
|
spec.add_development_dependency("bundler", ">= 1.17")
|
38
38
|
spec.add_development_dependency("minitest", "~> 5.0")
|
39
39
|
spec.add_development_dependency("rake", ">= 10.0")
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_todo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: prism
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
19
|
+
version: '0.15'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
26
|
+
version: '0.15'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -104,24 +104,21 @@ files:
|
|
104
104
|
- README.md
|
105
105
|
- Rakefile
|
106
106
|
- bin/console
|
107
|
+
- bin/profile
|
107
108
|
- bin/rubocop
|
108
109
|
- bin/setup
|
109
110
|
- dev.yml
|
110
111
|
- exe/smart_todo
|
111
112
|
- lib/smart_todo.rb
|
112
113
|
- lib/smart_todo/cli.rb
|
114
|
+
- lib/smart_todo/comment_parser.rb
|
113
115
|
- lib/smart_todo/dispatchers/base.rb
|
114
116
|
- lib/smart_todo/dispatchers/output.rb
|
115
117
|
- lib/smart_todo/dispatchers/slack.rb
|
116
118
|
- lib/smart_todo/events.rb
|
117
|
-
- lib/smart_todo/
|
118
|
-
- lib/smart_todo/events/gem_bump.rb
|
119
|
-
- lib/smart_todo/events/gem_release.rb
|
120
|
-
- lib/smart_todo/events/issue_close.rb
|
121
|
-
- lib/smart_todo/parser/comment_parser.rb
|
122
|
-
- lib/smart_todo/parser/metadata_parser.rb
|
123
|
-
- lib/smart_todo/parser/todo_node.rb
|
119
|
+
- lib/smart_todo/http_client_builder.rb
|
124
120
|
- lib/smart_todo/slack_client.rb
|
121
|
+
- lib/smart_todo/todo.rb
|
125
122
|
- lib/smart_todo/version.rb
|
126
123
|
- lib/smart_todo_cop.rb
|
127
124
|
- service.yml
|
@@ -150,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
147
|
- !ruby/object:Gem::Version
|
151
148
|
version: '0'
|
152
149
|
requirements: []
|
153
|
-
rubygems_version: 3.4.
|
150
|
+
rubygems_version: 3.4.22
|
154
151
|
signing_key:
|
155
152
|
specification_version: 4
|
156
153
|
summary: Enhance todo's comments in your codebase.
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "time"
|
4
|
-
|
5
|
-
module SmartTodo
|
6
|
-
module Events
|
7
|
-
# An event that check if the passed date is passed
|
8
|
-
class Date
|
9
|
-
class << self
|
10
|
-
# @param on_date [String] a string parsable by Time.parse
|
11
|
-
# @return [String, false]
|
12
|
-
def met?(on_date)
|
13
|
-
if Time.now >= Time.parse(on_date)
|
14
|
-
message(on_date)
|
15
|
-
else
|
16
|
-
false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# @param on_date [String]
|
21
|
-
# @return [String]
|
22
|
-
def message(on_date)
|
23
|
-
"We are past the *#{on_date}* due date and your TODO is now ready to be addressed."
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
gem("bundler")
|
4
|
-
require "bundler"
|
5
|
-
|
6
|
-
module SmartTodo
|
7
|
-
module Events
|
8
|
-
# An event that compare the version of a gem specified in your Gemfile.lock
|
9
|
-
# with the expected version specifiers.
|
10
|
-
class GemBump
|
11
|
-
# @param gem_name [String]
|
12
|
-
# @param requirements [Array] a list of version specifiers.
|
13
|
-
# The specifiers are the same as the one used in Gemfiles or Gemspecs
|
14
|
-
#
|
15
|
-
# @example Expecting a specific version
|
16
|
-
# GemBump.new('rails', ['6.0'])
|
17
|
-
#
|
18
|
-
# @example Expecting a version in the 5.x.x series
|
19
|
-
# GemBump.new('rails', ['> 5.2', '< 6'])
|
20
|
-
def initialize(gem_name, requirements)
|
21
|
-
@gem_name = gem_name
|
22
|
-
@requirements = Gem::Requirement.new(requirements)
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [String, false]
|
26
|
-
def met?
|
27
|
-
return error_message if spec_set[@gem_name].empty?
|
28
|
-
|
29
|
-
installed_version = spec_set[@gem_name].first.version
|
30
|
-
if @requirements.satisfied_by?(installed_version)
|
31
|
-
message(installed_version)
|
32
|
-
else
|
33
|
-
false
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
# Error message send to Slack in case a gem couldn't be found
|
38
|
-
#
|
39
|
-
# @return [String]
|
40
|
-
def error_message
|
41
|
-
"The gem *#{@gem_name}* is not in your dependencies, I can't determine if your TODO is ready to be addressed."
|
42
|
-
end
|
43
|
-
|
44
|
-
# @return [String]
|
45
|
-
def message(version_number)
|
46
|
-
"The gem *#{@gem_name}* was updated to version *#{version_number}* and your TODO is now ready to be addressed."
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# @return [Bundler::SpecSet] an instance of Bundler::SpecSet
|
52
|
-
def spec_set
|
53
|
-
@spec_set ||= Bundler.load.specs
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "net/http"
|
4
|
-
require "json"
|
5
|
-
|
6
|
-
module SmartTodo
|
7
|
-
module Events
|
8
|
-
# An event that check if a new version of gem has been released on RubyGem
|
9
|
-
# with the expected version specifiers.
|
10
|
-
# This event will make an API call to the RubyGem API
|
11
|
-
class GemRelease
|
12
|
-
# @param gem_name [String]
|
13
|
-
# @param requirements [Array] a list of version specifiers.
|
14
|
-
# The specifiers are the same as the one used in Gemfiles or Gemspecs
|
15
|
-
#
|
16
|
-
# @example Expecting a specific version
|
17
|
-
# GemRelease.new('rails', ['6.0'])
|
18
|
-
#
|
19
|
-
# @example Expecting a version in the 5.x.x series
|
20
|
-
# GemRelease.new('rails', ['> 5.2', '< 6'])
|
21
|
-
def initialize(gem_name, requirements)
|
22
|
-
@gem_name = gem_name
|
23
|
-
@requirements = Gem::Requirement.new(requirements)
|
24
|
-
end
|
25
|
-
|
26
|
-
# @return [String, false]
|
27
|
-
def met?
|
28
|
-
response = client.get("/api/v1/versions/#{@gem_name}.json")
|
29
|
-
|
30
|
-
if response.code_type < Net::HTTPClientError
|
31
|
-
error_message
|
32
|
-
elsif (gem = version_released?(response.body))
|
33
|
-
message(gem["number"])
|
34
|
-
else
|
35
|
-
false
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Error message send to Slack in case a gem couldn't be found
|
40
|
-
#
|
41
|
-
# @return [String]
|
42
|
-
def error_message
|
43
|
-
"The gem *#{@gem_name}* doesn't seem to exist, I can't determine if your TODO is ready to be addressed."
|
44
|
-
end
|
45
|
-
|
46
|
-
# @return [String]
|
47
|
-
def message(version_number)
|
48
|
-
"The gem *#{@gem_name}* was released to version *#{version_number}* and your TODO is now ready to be addressed."
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
# @param gem_versions [String] the response sent from RubyGems
|
54
|
-
# @return [true, false]
|
55
|
-
def version_released?(gem_versions)
|
56
|
-
JSON.parse(gem_versions).find do |gem|
|
57
|
-
@requirements.satisfied_by?(Gem::Version.new(gem["number"]))
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# @return [Net::HTTP] an instance of Net::HTTP
|
62
|
-
def client
|
63
|
-
@client ||= Net::HTTP.new("rubygems.org", Net::HTTP.https_default_port).tap do |client|
|
64
|
-
client.use_ssl = true
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
@@ -1,86 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "net/http"
|
4
|
-
require "json"
|
5
|
-
|
6
|
-
module SmartTodo
|
7
|
-
module Events
|
8
|
-
# An event that check if a GitHub Pull Request or Issue is closed.
|
9
|
-
# This event will make an API call to the GitHub API.
|
10
|
-
#
|
11
|
-
# If the Pull Request or Issue is on a private repository, exporting a token
|
12
|
-
# with the `repos` scope in the +SMART_TODO_GITHUB_TOKEN+ environment variable
|
13
|
-
# is required.
|
14
|
-
class IssueClose
|
15
|
-
TOKEN_ENV = "SMART_TODO_GITHUB_TOKEN"
|
16
|
-
|
17
|
-
# @param organization [String]
|
18
|
-
# @param repo [String]
|
19
|
-
# @param pr_number [String, Integer]
|
20
|
-
def initialize(organization, repo, pr_number, type:)
|
21
|
-
@url = "/repos/#{organization}/#{repo}/#{type}/#{pr_number}"
|
22
|
-
@organization = organization
|
23
|
-
@repo = repo
|
24
|
-
@pr_number = pr_number
|
25
|
-
end
|
26
|
-
|
27
|
-
# @return [String, false]
|
28
|
-
def met?
|
29
|
-
response = client.get(@url, default_headers)
|
30
|
-
|
31
|
-
if response.code_type < Net::HTTPClientError
|
32
|
-
error_message
|
33
|
-
elsif pull_request_closed?(response.body)
|
34
|
-
message
|
35
|
-
else
|
36
|
-
false
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# Error message send to Slack in case the Pull Request or Issue couldn't be found.
|
41
|
-
#
|
42
|
-
# @return [String]
|
43
|
-
def error_message
|
44
|
-
<<~EOM
|
45
|
-
I can't retrieve the information from the PR or Issue *#{@pr_number}* in the
|
46
|
-
*#{@organization}/#{@repo}* repository.
|
47
|
-
|
48
|
-
If the repository is a private one, make sure to export the `#{TOKEN_ENV}`
|
49
|
-
environment variable with a correct GitHub token.
|
50
|
-
EOM
|
51
|
-
end
|
52
|
-
|
53
|
-
# @return [String]
|
54
|
-
def message
|
55
|
-
<<~EOM
|
56
|
-
The Pull Request or Issue https://github.com/#{@organization}/#{@repo}/pull/#{@pr_number}
|
57
|
-
is now closed, your TODO is ready to be addressed.
|
58
|
-
EOM
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
# @return [Net::HTTP] an instance of Net::HTTP
|
64
|
-
def client
|
65
|
-
@client ||= Net::HTTP.new("api.github.com", Net::HTTP.https_default_port).tap do |client|
|
66
|
-
client.use_ssl = true
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
# @param pull_request [String] the Pull Request or Issue
|
71
|
-
# detail sent back from the GitHub API
|
72
|
-
#
|
73
|
-
# @return [true, false]
|
74
|
-
def pull_request_closed?(pull_request)
|
75
|
-
JSON.parse(pull_request)["state"] == "closed"
|
76
|
-
end
|
77
|
-
|
78
|
-
# @return [Hash]
|
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]
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
@@ -1,71 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "ripper"
|
4
|
-
|
5
|
-
module SmartTodo
|
6
|
-
module Parser
|
7
|
-
# This class is used to parse Ruby code and will stop each time
|
8
|
-
# a Ruby comment is encountered. It will detect if a TODO comment
|
9
|
-
# is a Smart Todo and will gather the comments associated to the TODO.
|
10
|
-
class CommentParser < Ripper::Filter
|
11
|
-
def initialize(*)
|
12
|
-
super
|
13
|
-
@node = nil
|
14
|
-
end
|
15
|
-
|
16
|
-
# @param comment [String] the actual Ruby comment
|
17
|
-
# @param data [Array<TodoNode>]
|
18
|
-
# @return [Array<TodoNode>]
|
19
|
-
def on_comment(comment, data)
|
20
|
-
if todo_metadata?(comment)
|
21
|
-
append_existing_node(data)
|
22
|
-
@node = TodoNode.new(comment)
|
23
|
-
elsif todo_comment?(comment)
|
24
|
-
@node << comment
|
25
|
-
else
|
26
|
-
append_existing_node(data)
|
27
|
-
@node = nil
|
28
|
-
end
|
29
|
-
|
30
|
-
data
|
31
|
-
end
|
32
|
-
|
33
|
-
# @param init [Array]
|
34
|
-
# @return [Array<TodoNode>]
|
35
|
-
def parse(init = [])
|
36
|
-
super(init)
|
37
|
-
|
38
|
-
init.tap { append_existing_node(init) }
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
# @param comment [String] the actual Ruby comment
|
44
|
-
# @return [nil, Integer]
|
45
|
-
def todo_metadata?(comment)
|
46
|
-
/^#\sTODO\(/ =~ comment
|
47
|
-
end
|
48
|
-
|
49
|
-
# Check if the comment is associated with the Smart Todo
|
50
|
-
# @param comment [String] the actual Ruby comment
|
51
|
-
# @return [true, false]
|
52
|
-
#
|
53
|
-
# @example When a comment is associated to a SmartTodo
|
54
|
-
# TODO(on_date(...), to: '...')
|
55
|
-
# This is an associated comment
|
56
|
-
#
|
57
|
-
# @example When a comment is not associated to a SmartTodo
|
58
|
-
# TODO(on_date(...), to: '...')
|
59
|
-
# This is an associated comment (Note the indentation)
|
60
|
-
def todo_comment?(comment)
|
61
|
-
@node&.indented_comment?(comment)
|
62
|
-
end
|
63
|
-
|
64
|
-
# @param data [Array<TodoNode>]
|
65
|
-
# @return [Array<TodoNode>]
|
66
|
-
def append_existing_node(data)
|
67
|
-
data << @node if @node
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
@@ -1,124 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "ripper"
|
4
|
-
|
5
|
-
module SmartTodo
|
6
|
-
module Parser
|
7
|
-
# A MethodNode represent an event associated to a TODO.
|
8
|
-
class MethodNode
|
9
|
-
attr_reader :method_name, :arguments
|
10
|
-
|
11
|
-
# @param method_name [Symbol]
|
12
|
-
# @param arguments [Array<String>]
|
13
|
-
def initialize(method_name, arguments)
|
14
|
-
@arguments = arguments
|
15
|
-
@method_name = method_name
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# This class is used to parse the ruby TODO() comment.
|
20
|
-
class MetadataParser < Ripper
|
21
|
-
class << self
|
22
|
-
# @param source [String] the actual Ruby code
|
23
|
-
def parse(source)
|
24
|
-
sexp = new(source).parse
|
25
|
-
Visitor.new.tap { |v| v.process(sexp) }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# @return [Array] an Array of Array
|
30
|
-
# the first element from each inner array is a token
|
31
|
-
def on_stmts_add(_, data)
|
32
|
-
data
|
33
|
-
end
|
34
|
-
|
35
|
-
# @param method [String] the name of the method
|
36
|
-
# when the parser hits one.
|
37
|
-
# @param args [Array]
|
38
|
-
# @return [Array, MethodNode]
|
39
|
-
def on_method_add_arg(method, args)
|
40
|
-
if method == "TODO"
|
41
|
-
args
|
42
|
-
else
|
43
|
-
MethodNode.new(method, args)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# @param list [nil, Array]
|
48
|
-
# @param arg [String]
|
49
|
-
# @return [Array]
|
50
|
-
def on_args_add(list, arg)
|
51
|
-
Array(list) << arg
|
52
|
-
end
|
53
|
-
|
54
|
-
# @param string_content [String]
|
55
|
-
# @return [String]
|
56
|
-
def on_string_add(_, string_content)
|
57
|
-
string_content
|
58
|
-
end
|
59
|
-
|
60
|
-
# @param key [String]
|
61
|
-
# @param value [String, Integer, MethodNode]
|
62
|
-
def on_assoc_new(key, value)
|
63
|
-
key.tr!(":", "")
|
64
|
-
|
65
|
-
case key
|
66
|
-
when "on"
|
67
|
-
[:on_todo_event, value]
|
68
|
-
when "to"
|
69
|
-
[:on_todo_assignee, value]
|
70
|
-
else
|
71
|
-
[:unknown, value]
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
# @param data [Hash]
|
76
|
-
# @return [Hash]
|
77
|
-
def on_bare_assoc_hash(data)
|
78
|
-
data
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
class Visitor
|
83
|
-
attr_reader :events, :assignees, :errors
|
84
|
-
|
85
|
-
def initialize
|
86
|
-
@events = []
|
87
|
-
@assignees = []
|
88
|
-
@errors = []
|
89
|
-
end
|
90
|
-
|
91
|
-
# Iterate over each tokens returned from the parser and call
|
92
|
-
# the corresponding method
|
93
|
-
#
|
94
|
-
# @param sexp [Array]
|
95
|
-
# @return [void]
|
96
|
-
def process(sexp)
|
97
|
-
return unless sexp
|
98
|
-
|
99
|
-
if sexp[0].is_a?(Array)
|
100
|
-
sexp.each { |node| process(node) }
|
101
|
-
else
|
102
|
-
method, *args = sexp
|
103
|
-
send(method, *args) if method.is_a?(Symbol) && respond_to?(method)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# @param method_node [MethodNode]
|
108
|
-
# @return [void]
|
109
|
-
def on_todo_event(method_node)
|
110
|
-
if method_node.is_a?(MethodNode)
|
111
|
-
events << method_node
|
112
|
-
else
|
113
|
-
errors << "Incorrect `:on` event format: #{method_node}"
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# @param assignee [String]
|
118
|
-
# @return [void]
|
119
|
-
def on_todo_assignee(assignee)
|
120
|
-
@assignees << assignee
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SmartTodo
|
4
|
-
module Parser
|
5
|
-
# Represents a SmartTodo which includes the associated events
|
6
|
-
# as well as the assignee.
|
7
|
-
class TodoNode
|
8
|
-
DEFAULT_RUBY_INDENTATION = 2
|
9
|
-
|
10
|
-
attr_reader :metadata
|
11
|
-
|
12
|
-
# @param todo [String] the actual Ruby comment
|
13
|
-
def initialize(todo)
|
14
|
-
@metadata = MetadataParser.parse(todo.gsub(/^#/, ""))
|
15
|
-
@comments = []
|
16
|
-
@start = todo.match(/^#(\s+)/)[1].size
|
17
|
-
end
|
18
|
-
|
19
|
-
# Return the associated comment for this TODO
|
20
|
-
#
|
21
|
-
# @return [String]
|
22
|
-
def comment
|
23
|
-
@comments.join
|
24
|
-
end
|
25
|
-
|
26
|
-
# @param comment [String]
|
27
|
-
# @return [void]
|
28
|
-
def <<(comment)
|
29
|
-
@comments << comment.gsub(/^#(\s+)/, "")
|
30
|
-
end
|
31
|
-
|
32
|
-
# Check if the +comment+ is indented two spaces below the
|
33
|
-
# TODO declaration. If yes the comment is considered to be part
|
34
|
-
# of the TODO itself. Otherwise it's just a regular comment.
|
35
|
-
#
|
36
|
-
# @param comment [String]
|
37
|
-
# @return [true, false]
|
38
|
-
def indented_comment?(comment)
|
39
|
-
comment.match(/^#(\s*)/)[1].size - @start == DEFAULT_RUBY_INDENTATION
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|