Skip to content

Commit aaf5d29

Browse files
committed
Speed
1 parent e328ecb commit aaf5d29

31 files changed

+541
-573
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ gemspec
99
group :rubocop do
1010
gem "rubocop-shopify", require: false
1111
end
12+
13+
gem "prism", github: "ruby/prism"

Gemfile.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
GIT
2+
remote: https://github.com/ruby/prism
3+
revision: 4ff8fe2f0f72ebdb52706b4898ccf20083fc92a3
4+
specs:
5+
prism (0.15.1)
6+
17
PATH
28
remote: .
39
specs:
410
smart_todo (1.6.0)
5-
rexml
11+
prism
612

713
GEM
814
remote: https://rubygems.org/
@@ -52,6 +58,7 @@ PLATFORMS
5258
DEPENDENCIES
5359
bundler (>= 1.17)
5460
minitest (~> 5.0)
61+
prism!
5562
rake (>= 10.0)
5663
rubocop-shopify
5764
smart_todo!

bin/profile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/smart_todo.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
# frozen_string_literal: true
22

3+
require "prism"
34
require "smart_todo/version"
45
require "smart_todo/events"
56

67
module SmartTodo
78
autoload :SlackClient, "smart_todo/slack_client"
89
autoload :CLI, "smart_todo/cli"
10+
autoload :Todo, "smart_todo/todo"
11+
autoload :CommentParser, "smart_todo/comment_parser"
912

10-
module Parser
11-
autoload :CommentParser, "smart_todo/parser/comment_parser"
12-
autoload :TodoNode, "smart_todo/parser/todo_node"
13-
autoload :MetadataParser, "smart_todo/parser/metadata_parser"
14-
end
15-
16-
module Events
13+
class Events
1714
autoload :Date, "smart_todo/events/date"
1815
autoload :GemBump, "smart_todo/events/gem_bump"
1916
autoload :GemRelease, "smart_todo/events/gem_release"

lib/smart_todo/cli.rb

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# frozen_string_literal: true
22

33
require "optionparser"
4+
require "etc"
45

56
module SmartTodo
67
# This class is the entrypoint of the SmartTodo library and is responsible
78
# to retrieve the command line options as well as iterating over each files/directories
89
# to run the +CommentParser+ on.
910
class CLI
10-
def initialize
11+
def initialize(dispatcher = nil)
1112
@options = {}
1213
@errors = []
14+
@dispatcher = dispatcher
1315
end
1416

1517
# @param args [Array<String>]
@@ -19,15 +21,22 @@ def run(args = ARGV)
1921

2022
paths << "." if paths.empty?
2123

24+
comment_parser = CommentParser.new
2225
paths.each do |path|
23-
normalize_path(path).each do |file|
24-
parse_file(file)
25-
26-
$stdout.print(".")
27-
$stdout.flush
26+
normalize_path(path).each do |filepath|
27+
comment_parser.parse_file(filepath)
28+
29+
# Don't print anything out in CI, as we want to go as fast as possible
30+
# and flushing stdout on every file can get quite costly.
31+
unless ENV["CI"]
32+
$stdout.print(".")
33+
$stdout.flush
34+
end
2835
end
2936
end
3037

38+
process_dispatches(process_todos(comment_parser.todos))
39+
3140
if @errors.empty?
3241
0
3342
else
@@ -79,25 +88,54 @@ def normalize_path(path)
7988
end
8089
end
8190

82-
# @param file [String] a path to a file
83-
def parse_file(file)
84-
Parser::CommentParser.new(File.read(file, encoding: "UTF-8")).parse.each do |todo_node|
91+
def process_todos(todos)
92+
events = Events.new
93+
dispatches = []
94+
95+
todos.each do |todo|
8596
event_message = nil
86-
event_met = todo_node.metadata.events.find do |event|
87-
event_message = Events.public_send(event.method_name, *event.arguments)
97+
event_met = todo.events.find do |event|
98+
event_message = events.public_send(event.method_name, *event.arguments)
8899
rescue => e
89-
message = "Error while parsing #{file} on event `#{event.method_name}` with arguments #{event.arguments}: " \
100+
message = "Error while parsing #{todo.filepath} on event `#{event.method_name}` " \
101+
"with arguments #{event.arguments.map(&:inspect)}: " \
90102
"#{e.message}"
91103

92104
@errors << message
93105

94106
nil
95107
end
96108

97-
@errors.concat(todo_node.metadata.errors)
98-
99-
dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
109+
@errors.concat(todo.errors)
110+
dispatches << [event_message, todo] if event_met
100111
end
112+
113+
dispatches
114+
end
115+
116+
def process_dispatches(dispatches)
117+
queue = Queue.new
118+
dispatches.each { |dispatch| queue << dispatch }
119+
120+
thread_count = Etc.nprocessors
121+
thread_count.times { queue << nil }
122+
123+
threads =
124+
thread_count.times.map do
125+
Thread.new do
126+
Thread.current.abort_on_exception = true
127+
128+
loop do
129+
dispatch = queue.pop
130+
break if dispatch.nil?
131+
132+
(event_message, todo) = dispatch
133+
dispatcher.new(event_message, todo, todo.filepath, @options).dispatch
134+
end
135+
end
136+
end
137+
138+
threads.each(&:join)
101139
end
102140
end
103141
end

lib/smart_todo/comment_parser.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
def parse(source, filepath = "-e")
12+
parse_comments(Prism.parse_inline_comments(source, filepath), filepath)
13+
end
14+
15+
def parse_file(filepath)
16+
parse_comments(Prism.parse_file_inline_comments(filepath), filepath)
17+
end
18+
19+
class << self
20+
def parse(source)
21+
parser = new
22+
parser.parse(source)
23+
parser.todos
24+
end
25+
end
26+
27+
private
28+
29+
def parse_comments(comments, filepath)
30+
current_todo = nil
31+
32+
comments.each do |comment|
33+
source = comment.location.slice
34+
35+
if source.match?(/^#\sTODO\(/)
36+
todos << current_todo if current_todo
37+
current_todo = Todo.new(source, filepath)
38+
elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2)
39+
current_todo << "#{source[(indent + 1)..]}\n"
40+
else
41+
todos << current_todo if current_todo
42+
current_todo = nil
43+
end
44+
end
45+
46+
todos << current_todo if current_todo
47+
end
48+
end
49+
end

lib/smart_todo/dispatchers/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def initialize(event_message, todo_node, file, options)
4040
@todo_node = todo_node
4141
@options = options
4242
@file = file
43-
@assignees = @todo_node.metadata.assignees
43+
@assignees = @todo_node.assignees
4444
end
4545

4646
# This method gets called when a TODO reminder is expired and needs to be delivered.

lib/smart_todo/events.rb

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# frozen_string_literal: true
22

3+
gem("bundler")
4+
require "bundler"
5+
require "net/http"
6+
require "time"
7+
require "json"
8+
39
module SmartTodo
410
# This module contains all the methods accessible for SmartTodo comments.
511
# It is meant to be reopened by the host application in order to define
@@ -10,23 +16,29 @@ module SmartTodo
1016
#
1117
# @example Adding a custom event
1218
# module SmartTodo
13-
# module Events
19+
# class Events
1420
# def trello_card_close(card)
1521
# ...
1622
# end
1723
# end
1824
# end
1925
#
2026
# TODO(on: trello_card_close(381), to: '[email protected]')
21-
module Events
22-
extend self
27+
class Events
28+
def initialize
29+
@now = nil
30+
@spec_set = nil
31+
@rubygems_client = nil
32+
@github_client = nil
33+
@installed_ruby_version = nil
34+
end
2335

2436
# Check if the +date+ is in the past
2537
#
2638
# @param date [String] a correctly formatted date
2739
# @return [false, String]
2840
def date(date)
29-
Date.met?(date)
41+
Date.met?(date, now)
3042
end
3143

3244
# Check if a new version of +gem_name+ was released with the +requirements+ expected
@@ -35,7 +47,7 @@ def date(date)
3547
# @param requirements [Array<String>] a list of version specifiers
3648
# @return [false, String]
3749
def gem_release(gem_name, *requirements)
38-
GemRelease.new(gem_name, requirements).met?
50+
GemRelease.new(gem_name, requirements, rubygems_client).met?
3951
end
4052

4153
# Check if +gem_name+ was bumped to the +requirements+ expected
@@ -44,7 +56,7 @@ def gem_release(gem_name, *requirements)
4456
# @param requirements [Array<String>] a list of version specifiers
4557
# @return [false, String]
4658
def gem_bump(gem_name, *requirements)
47-
GemBump.new(gem_name, requirements).met?
59+
GemBump.new(gem_name, requirements, spec_set).met?
4860
end
4961

5062
# Check if the issue +issue_number+ is closed
@@ -54,7 +66,7 @@ def gem_bump(gem_name, *requirements)
5466
# @param issue_number [String, Integer]
5567
# @return [false, String]
5668
def issue_close(organization, repo, issue_number)
57-
IssueClose.new(organization, repo, issue_number, type: "issues").met?
69+
IssueClose.new(organization, repo, issue_number, github_client, type: "issues").met?
5870
end
5971

6072
# Check if the pull request +pr_number+ is closed
@@ -64,15 +76,41 @@ def issue_close(organization, repo, issue_number)
6476
# @param pr_number [String, Integer]
6577
# @return [false, String]
6678
def pull_request_close(organization, repo, pr_number)
67-
IssueClose.new(organization, repo, pr_number, type: "pulls").met?
79+
IssueClose.new(organization, repo, pr_number, github_client, type: "pulls").met?
6880
end
6981

7082
# Check if the installed ruby version meets requirements.
7183
#
7284
# @param requirements [Array<String>] a list of version specifiers
7385
# @return [false, String]
7486
def ruby_version(*requirements)
75-
RubyVersion.new(requirements).met?
87+
RubyVersion.new(requirements, installed_ruby_version).met?
88+
end
89+
90+
private
91+
92+
def now
93+
@now ||= Time.now
94+
end
95+
96+
def spec_set
97+
@spec_set ||= Bundler.load.specs
98+
end
99+
100+
def rubygems_client
101+
@rubygems_client ||= Net::HTTP.new("rubygems.org", Net::HTTP.https_default_port).tap do |client|
102+
client.use_ssl = true
103+
end
104+
end
105+
106+
def github_client
107+
@github_client ||= Net::HTTP.new("api.github.com", Net::HTTP.https_default_port).tap do |client|
108+
client.use_ssl = true
109+
end
110+
end
111+
112+
def installed_ruby_version
113+
@installed_ruby_version ||= Gem::Version.new(RUBY_VERSION)
76114
end
77115
end
78116
end

lib/smart_todo/events/date.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
require "time"
44

55
module SmartTodo
6-
module Events
6+
class Events
77
# An event that check if the passed date is passed
88
class Date
99
class << self
1010
# @param on_date [String] a string parsable by Time.parse
1111
# @return [String, false]
12-
def met?(on_date)
13-
if Time.now >= Time.parse(on_date)
12+
def met?(on_date, now = Time.now)
13+
if now >= Time.parse(on_date)
1414
message(on_date)
1515
else
1616
false

0 commit comments

Comments
 (0)