diff --git a/Gemfile b/Gemfile index 942ec5e..7110767 100644 --- a/Gemfile +++ b/Gemfile @@ -9,3 +9,5 @@ gemspec group :rubocop do gem "rubocop-shopify", require: false end + +gem "prism", github: "ruby/prism" diff --git a/Gemfile.lock b/Gemfile.lock index 688f9c5..73b6963 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,14 @@ +GIT + remote: https://github.com/ruby/prism + revision: 4ff8fe2f0f72ebdb52706b4898ccf20083fc92a3 + specs: + prism (0.15.1) + PATH remote: . specs: smart_todo (1.6.0) - rexml + prism GEM remote: https://rubygems.org/ @@ -52,6 +58,7 @@ PLATFORMS DEPENDENCIES bundler (>= 1.17) minitest (~> 5.0) + prism! rake (>= 10.0) rubocop-shopify smart_todo! diff --git a/bin/profile b/bin/profile new file mode 100755 index 0000000..c10accc --- /dev/null +++ b/bin/profile @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "smart_todo" + +class NullDispatcher < SmartTodo::Dispatchers::Base + class << self + def validate_options!(_); end + end + + def dispatch + end +end + +exit SmartTodo::CLI.new(NullDispatcher).run diff --git a/lib/smart_todo.rb b/lib/smart_todo.rb index 631e7d6..afb9994 100644 --- a/lib/smart_todo.rb +++ b/lib/smart_todo.rb @@ -1,19 +1,16 @@ # frozen_string_literal: true +require "prism" require "smart_todo/version" require "smart_todo/events" module SmartTodo autoload :SlackClient, "smart_todo/slack_client" autoload :CLI, "smart_todo/cli" + autoload :Todo, "smart_todo/todo" + autoload :CommentParser, "smart_todo/comment_parser" - module Parser - autoload :CommentParser, "smart_todo/parser/comment_parser" - autoload :TodoNode, "smart_todo/parser/todo_node" - autoload :MetadataParser, "smart_todo/parser/metadata_parser" - end - - module Events + class Events autoload :Date, "smart_todo/events/date" autoload :GemBump, "smart_todo/events/gem_bump" autoload :GemRelease, "smart_todo/events/gem_release" diff --git a/lib/smart_todo/cli.rb b/lib/smart_todo/cli.rb index a919bb4..fee8256 100644 --- a/lib/smart_todo/cli.rb +++ b/lib/smart_todo/cli.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true require "optionparser" +require "etc" module SmartTodo # This class is the entrypoint of the SmartTodo library and is responsible # to retrieve the command line options as well as iterating over each files/directories # to run the +CommentParser+ on. class CLI - def initialize + def initialize(dispatcher = nil) @options = {} @errors = [] + @dispatcher = dispatcher end # @param args [Array] @@ -19,15 +21,22 @@ def run(args = ARGV) paths << "." if paths.empty? + comment_parser = CommentParser.new paths.each do |path| - normalize_path(path).each do |file| - parse_file(file) - - $stdout.print(".") - $stdout.flush + normalize_path(path).each do |filepath| + comment_parser.parse_file(filepath) + + # Don't print anything out in CI, as we want to go as fast as possible + # and flushing stdout on every file can get quite costly. + unless ENV["CI"] + $stdout.print(".") + $stdout.flush + end end end + process_dispatches(process_todos(comment_parser.todos)) + if @errors.empty? 0 else @@ -79,14 +88,17 @@ def normalize_path(path) end end - # @param file [String] a path to a file - def parse_file(file) - Parser::CommentParser.new(File.read(file, encoding: "UTF-8")).parse.each do |todo_node| + def process_todos(todos) + events = Events.new + dispatches = [] + + todos.each do |todo| event_message = nil - event_met = todo_node.metadata.events.find do |event| - event_message = Events.public_send(event.method_name, *event.arguments) + event_met = todo.events.find do |event| + event_message = events.public_send(event.method_name, *event.arguments) rescue => e - message = "Error while parsing #{file} on event `#{event.method_name}` with arguments #{event.arguments}: " \ + message = "Error while parsing #{todo.filepath} on event `#{event.method_name}` " \ + "with arguments #{event.arguments.map(&:inspect)}: " \ "#{e.message}" @errors << message @@ -94,10 +106,36 @@ def parse_file(file) nil end - @errors.concat(todo_node.metadata.errors) - - dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met + @errors.concat(todo.errors) + dispatches << [event_message, todo] if event_met end + + dispatches + end + + def process_dispatches(dispatches) + queue = Queue.new + dispatches.each { |dispatch| queue << dispatch } + + thread_count = Etc.nprocessors + thread_count.times { queue << nil } + + threads = + thread_count.times.map do + Thread.new do + Thread.current.abort_on_exception = true + + loop do + dispatch = queue.pop + break if dispatch.nil? + + (event_message, todo) = dispatch + dispatcher.new(event_message, todo, todo.filepath, @options).dispatch + end + end + end + + threads.each(&:join) end end end diff --git a/lib/smart_todo/comment_parser.rb b/lib/smart_todo/comment_parser.rb new file mode 100644 index 0000000..83c7834 --- /dev/null +++ b/lib/smart_todo/comment_parser.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module SmartTodo + class CommentParser + attr_reader :todos + + def initialize + @todos = [] + end + + def parse(source, filepath = "-e") + parse_comments(Prism.parse_inline_comments(source, filepath), filepath) + end + + def parse_file(filepath) + parse_comments(Prism.parse_file_inline_comments(filepath), filepath) + end + + class << self + def parse(source) + parser = new + parser.parse(source) + parser.todos + end + end + + private + + def parse_comments(comments, filepath) + current_todo = nil + + comments.each do |comment| + source = comment.location.slice + + if source.match?(/^#\sTODO\(/) + todos << current_todo if current_todo + current_todo = Todo.new(source, filepath) + elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2) + current_todo << "#{source[(indent + 1)..]}\n" + else + todos << current_todo if current_todo + current_todo = nil + end + end + + todos << current_todo if current_todo + end + end +end diff --git a/lib/smart_todo/dispatchers/base.rb b/lib/smart_todo/dispatchers/base.rb index 7d88d10..51f706f 100644 --- a/lib/smart_todo/dispatchers/base.rb +++ b/lib/smart_todo/dispatchers/base.rb @@ -40,7 +40,7 @@ def initialize(event_message, todo_node, file, options) @todo_node = todo_node @options = options @file = file - @assignees = @todo_node.metadata.assignees + @assignees = @todo_node.assignees end # This method gets called when a TODO reminder is expired and needs to be delivered. diff --git a/lib/smart_todo/events.rb b/lib/smart_todo/events.rb index 820ee23..6ce578f 100644 --- a/lib/smart_todo/events.rb +++ b/lib/smart_todo/events.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +gem("bundler") +require "bundler" +require "net/http" +require "time" +require "json" + module SmartTodo # This module contains all the methods accessible for SmartTodo comments. # It is meant to be reopened by the host application in order to define @@ -10,7 +16,7 @@ module SmartTodo # # @example Adding a custom event # module SmartTodo - # module Events + # class Events # def trello_card_close(card) # ... # end @@ -18,15 +24,21 @@ module SmartTodo # end # # TODO(on: trello_card_close(381), to: 'john@example.com') - module Events - extend self + class Events + def initialize + @now = nil + @spec_set = nil + @rubygems_client = nil + @github_client = nil + @installed_ruby_version = nil + end # Check if the +date+ is in the past # # @param date [String] a correctly formatted date # @return [false, String] def date(date) - Date.met?(date) + Date.met?(date, now) end # Check if a new version of +gem_name+ was released with the +requirements+ expected @@ -35,7 +47,7 @@ def date(date) # @param requirements [Array] a list of version specifiers # @return [false, String] def gem_release(gem_name, *requirements) - GemRelease.new(gem_name, requirements).met? + GemRelease.new(gem_name, requirements, rubygems_client).met? end # Check if +gem_name+ was bumped to the +requirements+ expected @@ -44,7 +56,7 @@ def gem_release(gem_name, *requirements) # @param requirements [Array] a list of version specifiers # @return [false, String] def gem_bump(gem_name, *requirements) - GemBump.new(gem_name, requirements).met? + GemBump.new(gem_name, requirements, spec_set).met? end # Check if the issue +issue_number+ is closed @@ -54,7 +66,7 @@ def gem_bump(gem_name, *requirements) # @param issue_number [String, Integer] # @return [false, String] def issue_close(organization, repo, issue_number) - IssueClose.new(organization, repo, issue_number, type: "issues").met? + IssueClose.new(organization, repo, issue_number, github_client, type: "issues").met? end # Check if the pull request +pr_number+ is closed @@ -64,7 +76,7 @@ def issue_close(organization, repo, issue_number) # @param pr_number [String, Integer] # @return [false, String] def pull_request_close(organization, repo, pr_number) - IssueClose.new(organization, repo, pr_number, type: "pulls").met? + IssueClose.new(organization, repo, pr_number, github_client, type: "pulls").met? end # Check if the installed ruby version meets requirements. @@ -72,7 +84,33 @@ def pull_request_close(organization, repo, pr_number) # @param requirements [Array] a list of version specifiers # @return [false, String] def ruby_version(*requirements) - RubyVersion.new(requirements).met? + RubyVersion.new(requirements, installed_ruby_version).met? + end + + private + + def now + @now ||= Time.now + end + + def spec_set + @spec_set ||= Bundler.load.specs + end + + def rubygems_client + @rubygems_client ||= Net::HTTP.new("rubygems.org", Net::HTTP.https_default_port).tap do |client| + client.use_ssl = true + end + end + + def github_client + @github_client ||= Net::HTTP.new("api.github.com", Net::HTTP.https_default_port).tap do |client| + client.use_ssl = true + end + end + + def installed_ruby_version + @installed_ruby_version ||= Gem::Version.new(RUBY_VERSION) end end end diff --git a/lib/smart_todo/events/date.rb b/lib/smart_todo/events/date.rb index 2ebc8f7..548a22b 100644 --- a/lib/smart_todo/events/date.rb +++ b/lib/smart_todo/events/date.rb @@ -3,14 +3,14 @@ require "time" module SmartTodo - module Events + class Events # An event that check if the passed date is passed class Date class << self # @param on_date [String] a string parsable by Time.parse # @return [String, false] - def met?(on_date) - if Time.now >= Time.parse(on_date) + def met?(on_date, now = Time.now) + if now >= Time.parse(on_date) message(on_date) else false diff --git a/lib/smart_todo/events/gem_bump.rb b/lib/smart_todo/events/gem_bump.rb index ea5a65f..0cb91d6 100644 --- a/lib/smart_todo/events/gem_bump.rb +++ b/lib/smart_todo/events/gem_bump.rb @@ -4,7 +4,7 @@ require "bundler" module SmartTodo - module Events + class Events # An event that compare the version of a gem specified in your Gemfile.lock # with the expected version specifiers. class GemBump @@ -17,16 +17,17 @@ class GemBump # # @example Expecting a version in the 5.x.x series # GemBump.new('rails', ['> 5.2', '< 6']) - def initialize(gem_name, requirements) + def initialize(gem_name, requirements, spec_set = Bundler.load.specs) @gem_name = gem_name @requirements = Gem::Requirement.new(requirements) + @spec_set = spec_set end # @return [String, false] def met? - return error_message if spec_set[@gem_name].empty? + return error_message if @spec_set[@gem_name].empty? - installed_version = spec_set[@gem_name].first.version + installed_version = @spec_set[@gem_name].first.version if @requirements.satisfied_by?(installed_version) message(installed_version) else @@ -45,13 +46,6 @@ def error_message def message(version_number) "The gem *#{@gem_name}* was updated to version *#{version_number}* and your TODO is now ready to be addressed." end - - private - - # @return [Bundler::SpecSet] an instance of Bundler::SpecSet - def spec_set - @spec_set ||= Bundler.load.specs - end end end end diff --git a/lib/smart_todo/events/gem_release.rb b/lib/smart_todo/events/gem_release.rb index 1b5cec3..5b2b586 100644 --- a/lib/smart_todo/events/gem_release.rb +++ b/lib/smart_todo/events/gem_release.rb @@ -4,7 +4,7 @@ require "json" module SmartTodo - module Events + class Events # An event that check if a new version of gem has been released on RubyGem # with the expected version specifiers. # This event will make an API call to the RubyGem API @@ -18,9 +18,10 @@ class GemRelease # # @example Expecting a version in the 5.x.x series # GemRelease.new('rails', ['> 5.2', '< 6']) - def initialize(gem_name, requirements) + def initialize(gem_name, requirements, client = nil) @gem_name = gem_name @requirements = Gem::Requirement.new(requirements) + @client = client end # @return [String, false] diff --git a/lib/smart_todo/events/issue_close.rb b/lib/smart_todo/events/issue_close.rb index 3ddda52..8266c25 100644 --- a/lib/smart_todo/events/issue_close.rb +++ b/lib/smart_todo/events/issue_close.rb @@ -4,7 +4,7 @@ require "json" module SmartTodo - module Events + class Events # An event that check if a GitHub Pull Request or Issue is closed. # This event will make an API call to the GitHub API. # @@ -22,11 +22,12 @@ class IssueClose # @param organization [String] # @param repo [String] # @param pr_number [String, Integer] - def initialize(organization, repo, pr_number, type:) + def initialize(organization, repo, pr_number, client = nil, type:) @url = "/repos/#{organization}/#{repo}/#{type}/#{pr_number}" @organization = organization @repo = repo @pr_number = pr_number + @client = client end # @return [String, false] diff --git a/lib/smart_todo/events/ruby_version.rb b/lib/smart_todo/events/ruby_version.rb index b82f247..9e07e94 100644 --- a/lib/smart_todo/events/ruby_version.rb +++ b/lib/smart_todo/events/ruby_version.rb @@ -1,20 +1,23 @@ # frozen_string_literal: true module SmartTodo - module Events + class Events # An event that checks the currently installed ruby version. # @example # RubyVersion.new(['>= 2.3', '< 3']) class RubyVersion - def initialize(requirements) + attr_reader :installed_ruby_version + + def initialize(requirements, installed_ruby_version = Gem::Version.new(RUBY_VERSION)) @requirements = Gem::Requirement.new(requirements) + @installed_ruby_version = installed_ruby_version end # @param requirements [Array] a list of version specifiers # @return [String, false] def met? - if @requirements.satisfied_by?(Gem::Version.new(installed_ruby_version)) - message(installed_ruby_version) + if @requirements.satisfied_by?(installed_ruby_version) + message else false end @@ -22,15 +25,9 @@ def met? # @param installed_ruby_version [String], requirements [String] # @return [String] - def message(installed_ruby_version) + def message "The currently installed version of Ruby #{installed_ruby_version} is #{@requirements}." end - - private - - def installed_ruby_version - RUBY_VERSION - end end end end diff --git a/lib/smart_todo/parser/comment_parser.rb b/lib/smart_todo/parser/comment_parser.rb deleted file mode 100644 index b718164..0000000 --- a/lib/smart_todo/parser/comment_parser.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "ripper" - -module SmartTodo - module Parser - # This class is used to parse Ruby code and will stop each time - # a Ruby comment is encountered. It will detect if a TODO comment - # is a Smart Todo and will gather the comments associated to the TODO. - class CommentParser < Ripper::Filter - def initialize(*) - super - @node = nil - end - - # @param comment [String] the actual Ruby comment - # @param data [Array] - # @return [Array] - def on_comment(comment, data) - if todo_metadata?(comment) - append_existing_node(data) - @node = TodoNode.new(comment) - elsif todo_comment?(comment) - @node << comment - else - append_existing_node(data) - @node = nil - end - - data - end - - # @param init [Array] - # @return [Array] - def parse(init = []) - super(init) - - init.tap { append_existing_node(init) } - end - - private - - # @param comment [String] the actual Ruby comment - # @return [nil, Integer] - def todo_metadata?(comment) - /^#\sTODO\(/ =~ comment - end - - # Check if the comment is associated with the Smart Todo - # @param comment [String] the actual Ruby comment - # @return [true, false] - # - # @example When a comment is associated to a SmartTodo - # TODO(on_date(...), to: '...') - # This is an associated comment - # - # @example When a comment is not associated to a SmartTodo - # TODO(on_date(...), to: '...') - # This is an associated comment (Note the indentation) - def todo_comment?(comment) - @node&.indented_comment?(comment) - end - - # @param data [Array] - # @return [Array] - def append_existing_node(data) - data << @node if @node - end - end - end -end diff --git a/lib/smart_todo/parser/metadata_parser.rb b/lib/smart_todo/parser/metadata_parser.rb deleted file mode 100644 index 0b51d8f..0000000 --- a/lib/smart_todo/parser/metadata_parser.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -require "ripper" - -module SmartTodo - module Parser - # A MethodNode represent an event associated to a TODO. - class MethodNode - attr_reader :method_name, :arguments - - # @param method_name [Symbol] - # @param arguments [Array] - def initialize(method_name, arguments) - @arguments = arguments - @method_name = method_name - end - end - - # This class is used to parse the ruby TODO() comment. - class MetadataParser < Ripper - class << self - # @param source [String] the actual Ruby code - def parse(source) - sexp = new(source).parse - Visitor.new.tap { |v| v.process(sexp) } - end - end - - # @return [Array] an Array of Array - # the first element from each inner array is a token - def on_stmts_add(_, data) - data - end - - # @param method [String] the name of the method - # when the parser hits one. - # @param args [Array] - # @return [Array, MethodNode] - def on_method_add_arg(method, args) - if method.start_with?(/TODO\W?/) - args - else - MethodNode.new(method, args) - end - end - - # @param list [nil, Array] - # @param arg [String] - # @return [Array] - def on_args_add(list, arg) - Array(list) << arg - end - - # @param string_content [String] - # @return [String] - def on_string_add(_, string_content) - string_content - end - - # @param key [String] - # @param value [String, Integer, MethodNode] - def on_assoc_new(key, value) - key.tr!(":", "") - - case key - when "on" - [:on_todo_event, value] - when "to" - [:on_todo_assignee, value] - else - [:unknown, value] - end - end - - # @param data [Hash] - # @return [Hash] - def on_bare_assoc_hash(data) - data - end - end - - class Visitor - attr_reader :events, :assignees, :errors - - def initialize - @events = [] - @assignees = [] - @errors = [] - end - - # Iterate over each tokens returned from the parser and call - # the corresponding method - # - # @param sexp [Array] - # @return [void] - def process(sexp) - return unless sexp - - if sexp[0].is_a?(Array) - sexp.each { |node| process(node) } - else - method, *args = sexp - send(method, *args) if method.is_a?(Symbol) && respond_to?(method) - end - end - - # @param method_node [MethodNode] - # @return [void] - def on_todo_event(method_node) - if method_node.is_a?(MethodNode) - events << method_node - else - errors << "Incorrect `:on` event format: #{method_node}" - end - end - - # @param assignee [String] - # @return [void] - def on_todo_assignee(assignee) - @assignees << assignee - end - end - end -end diff --git a/lib/smart_todo/parser/todo_node.rb b/lib/smart_todo/parser/todo_node.rb deleted file mode 100644 index 4992568..0000000 --- a/lib/smart_todo/parser/todo_node.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module SmartTodo - module Parser - # Represents a SmartTodo which includes the associated events - # as well as the assignee. - class TodoNode - DEFAULT_RUBY_INDENTATION = 2 - - attr_reader :metadata - - # @param todo [String] the actual Ruby comment - def initialize(todo) - @metadata = MetadataParser.parse(todo.gsub(/^#/, "")) - @comments = [] - @start = todo.match(/^#(\s+)/)[1].size - end - - # Return the associated comment for this TODO - # - # @return [String] - def comment - @comments.join - end - - # @param comment [String] - # @return [void] - def <<(comment) - @comments << comment.gsub(/^#(\s+)/, "") - end - - # Check if the +comment+ is indented two spaces below the - # TODO declaration. If yes the comment is considered to be part - # of the TODO itself. Otherwise it's just a regular comment. - # - # @param comment [String] - # @return [true, false] - def indented_comment?(comment) - comment.match(/^#(\s*)/)[1].size - @start == DEFAULT_RUBY_INDENTATION - end - end - end -end diff --git a/lib/smart_todo/todo.rb b/lib/smart_todo/todo.rb new file mode 100644 index 0000000..51bdcb2 --- /dev/null +++ b/lib/smart_todo/todo.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module SmartTodo + class Todo + attr_reader :filepath, :comment, :indent + attr_reader :events, :assignees, :errors + + def initialize(source, filepath = "-e") + @filepath = filepath + @comment = +"" + @indent = source[/^#(\s+)/, 1].length + + @events = [] + @assignees = [] + @errors = [] + + parse(source[(indent + 1)..]) + end + + def <<(source) + comment << source + end + + class CallNode + attr_reader :method_name, :arguments, :location + + def initialize(method_name, arguments, location) + @arguments = arguments + @method_name = method_name + @location = location + end + end + + class Compiler < Prism::Compiler + attr_reader :metadata + + def initialize(metadata) + super() + @metadata = metadata + end + + def visit_call_node(node) + CallNode.new(node.name, visit_all(node.arguments&.arguments || []), node.location) + end + + def visit_integer_node(node) + node.value + end + + def visit_keyword_hash_node(node) + node.elements.each do |element| + next unless (key = element.key).is_a?(Prism::SymbolNode) + + case key.unescaped.to_sym + when :on + value = visit(element.value) + + if value.is_a?(CallNode) + if value.arguments.all? { |arg| arg.is_a?(Integer) || arg.is_a?(String) } + metadata.events << value + else + metadata.errors << "Incorrect `:on` event format: #{value.location.slice}" + end + else + metadata.errors << "Incorrect `:on` event format: #{value.inspect}" + end + when :to + metadata.assignees << visit(element.value) + end + end + end + + def visit_string_node(node) + node.unescaped + end + end + + private + + def parse(source) + Prism.parse(source).value.statements.body.first.accept(Compiler.new(self)) + end + end +end diff --git a/lib/smart_todo_cop.rb b/lib/smart_todo_cop.rb index c434c4f..9a0b68d 100644 --- a/lib/smart_todo_cop.rb +++ b/lib/smart_todo_cop.rb @@ -17,7 +17,7 @@ class SmartTodoCop < Cop # @return [void] def investigate(processed_source) processed_source.comments.each do |comment| - next unless /^#\sTODO/ =~ comment.text + next unless /^#\sTODO/.match?(comment.text) metadata = metadata(comment.text) @@ -36,21 +36,21 @@ def investigate(processed_source) # @param comment [String] # @return [SmartTodo::Parser::Visitor] def metadata(comment) - ::SmartTodo::Parser::MetadataParser.parse(comment.gsub(/^#/, "")) + ::SmartTodo::Todo.new(comment) end # @param metadata [SmartTodo::Parser::Visitor] # @return [true, false] def smart_todo?(metadata) metadata.events.any? && - metadata.events.all? { |event| event.is_a?(::SmartTodo::Parser::MethodNode) } && + metadata.events.all? { |event| event.is_a?(::SmartTodo::Todo::CallNode) } && metadata.assignees.any? end # @param metadata [Array] # @return [Array] def invalid_event_methods(events) - events.map(&:method_name).reject { |method| ::SmartTodo::Events.respond_to?(method) } + events.map(&:method_name).reject { |method| ::SmartTodo::Events.method_defined?(method) } end end end diff --git a/smart_todo.gemspec b/smart_todo.gemspec index 5d2f4a1..5ebc21c 100644 --- a/smart_todo.gemspec +++ b/smart_todo.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |spec| spec.executables = ["smart_todo"] spec.require_paths = ["lib"] - spec.add_runtime_dependency("rexml") + spec.add_runtime_dependency("prism") spec.add_development_dependency("bundler", ">= 1.17") spec.add_development_dependency("minitest", "~> 5.0") spec.add_development_dependency("rake", ">= 10.0") diff --git a/test/smart_todo/cli_test.rb b/test/smart_todo/cli_test.rb index 24992cc..92eddee 100644 --- a/test/smart_todo/cli_test.rb +++ b/test/smart_todo/cli_test.rb @@ -69,30 +69,36 @@ def hello end def test_ascii_encoded_file_with_utf8_characters_can_be_parsed_correctly + previous_verbose = $VERBOSE previous_encoding = Encoding.default_external - Encoding.default_external = "US-ASCII" - cli = CLI.new - ruby_code = <<~EOM - # See "市区町村名" - def hello - end + begin + $VERBOSE = nil + Encoding.default_external = "US-ASCII" - # TODO(on: date('2070-03-02'), to: '#general') - # See "市区町村名" - def hello - end - EOM + cli = CLI.new + ruby_code = <<~EOM + # See "市区町村名" + def hello + end - generate_ruby_file(ruby_code) do |file| - assert_output(".") do - assert_equal(0, cli.run([file.path, "--slack_token", "123", "--fallback_channel", '#general"'])) + # TODO(on: date('2070-03-02'), to: '#general') + # See "市区町村名" + def hello + end + EOM + + generate_ruby_file(ruby_code) do |file| + assert_output(".") do + assert_equal(0, cli.run([file.path, "--slack_token", "123", "--fallback_channel", '#general"'])) + end end - end - assert_not_requested(:post, /chat.postMessage/) - ensure - Encoding.default_external = previous_encoding + assert_not_requested(:post, /chat.postMessage/) + ensure + Encoding.default_external = previous_encoding + $VERBOSE = previous_verbose + end end def test_does_not_crash_if_the_event_is_incorrectly_formatted @@ -104,7 +110,7 @@ def hello EOM generate_ruby_file(ruby_code) do |file| - assert_output(".", /Incorrect `:on` event format: 2010-03-02/) do + assert_output(".", /Incorrect `:on` event format: "2010-03-02"/) do assert_equal(1, cli.run([file.path, "--slack_token", "123", "--fallback_channel", '#general"'])) end end @@ -121,7 +127,7 @@ def hello EOM generate_ruby_file(ruby_code) do |file| - assert_output(".", /Error while parsing .* on event `date` with arguments \["2010"\]: argument out of range/) do + assert_output(".", /Incorrect `:on` event format: date\(2010-03-02\)/) do assert_equal(1, cli.run([file.path, "--slack_token", "123", "--fallback_channel", '#general"'])) end end diff --git a/test/smart_todo/comment_parser_test.rb b/test/smart_todo/comment_parser_test.rb new file mode 100644 index 0000000..6c3e6e5 --- /dev/null +++ b/test/smart_todo/comment_parser_test.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require "test_helper" + +module SmartTodo + class CommentParserTest < Minitest::Test + def test_parse_one_todo_with_single_line_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Remove this code once done + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal("Remove this code once done\n", todo[0].comment) + end + + def test_parse_multiple_todo_with_single_line_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Remove this code once done + def hello + end + + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Remove this code once done + def bar + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(2, todo.size) + assert_equal("Remove this code once done\n", todo[0].comment) + assert_equal("Remove this code once done\n", todo[1].comment) + end + + def test_parse_one_todo_with_multi_line_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Remove this code once done + # This is important + # Please don't disappoint me + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal(<<~EOM, todo[0].comment) + Remove this code once done + This is important + Please don't disappoint me + EOM + end + + def test_parse_multiple_todo_with_multi_line_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Remove this code once done + # This is important + # Please don't disappoint me + def hello + end + + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Hello World + # Good Bye! + def bar + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(2, todo.size) + assert_equal(<<~EOM, todo[0].comment) + Remove this code once done + This is important + Please don't disappoint me + EOM + assert_equal(<<~EOM, todo[1].comment) + Hello World + Good Bye! + EOM + end + + def test_parse_no_todo + ruby_code = <<~RUBY + # This is a regular comment + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_empty(todo) + end + + def test_parse_todo_with_no_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal("", todo[0].comment) + end + + def test_parse_todo_with_unindented_comment + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # Oups comment is not indented to the TODO + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal("", todo[0].comment) + end + + def test_parse_todo_with_weird_comment_indentation + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + #bla + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal("", todo[0].comment) + end + + def test_parse_todo_with_nothing_else + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + # The rest of the file is completely empty + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(1, todo.size) + assert_equal("The rest of the file is completely empty\n", todo[0].comment) + end + + def test_parse_no_comment_at_all + ruby_code = <<~RUBY + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_empty(todo) + end + + def test_parse_todo_and_creates_metadata + ruby_code = <<~RUBY + # TODO(on: date('2019-08-04'), to: 'john@example.com') + def hello + end + RUBY + + todo = CommentParser.parse(ruby_code) + assert_equal(:date, todo[0].events[0].method_name) + assert_equal(["john@example.com"], todo[0].assignees) + end + end +end diff --git a/test/smart_todo/dispatchers/output_test.rb b/test/smart_todo/dispatchers/output_test.rb index 4048598..c6285eb 100644 --- a/test/smart_todo/dispatchers/output_test.rb +++ b/test/smart_todo/dispatchers/output_test.rb @@ -39,7 +39,7 @@ def hello end EOM - Parser::CommentParser.new(ruby_code).parse[0] + CommentParser.parse(ruby_code)[0] end end end diff --git a/test/smart_todo/dispatchers/slack_test.rb b/test/smart_todo/dispatchers/slack_test.rb index 01e5518..c006d09 100644 --- a/test/smart_todo/dispatchers/slack_test.rb +++ b/test/smart_todo/dispatchers/slack_test.rb @@ -159,7 +159,7 @@ def hello end EOM - Parser::CommentParser.new(ruby_code).parse[0] + CommentParser.parse(ruby_code)[0] end end end diff --git a/test/smart_todo/events/date_test.rb b/test/smart_todo/events/date_test.rb index e02e222..580a1a6 100644 --- a/test/smart_todo/events/date_test.rb +++ b/test/smart_todo/events/date_test.rb @@ -4,7 +4,7 @@ require "time" module SmartTodo - module Events + class Events class DateTest < Minitest::Test def test_when_date_is_in_the_past Time.stub(:now, Time.parse("2019-07-04 02:57:18 +0000")) do diff --git a/test/smart_todo/events/gem_bump_test.rb b/test/smart_todo/events/gem_bump_test.rb index f115266..eb68ab5 100644 --- a/test/smart_todo/events/gem_bump_test.rb +++ b/test/smart_todo/events/gem_bump_test.rb @@ -4,51 +4,41 @@ require "bundler" module SmartTodo - module Events + class Events class GemBumpTest < Minitest::Test def test_when_gem_is_released - bump = GemBump.new("rubocop", "0.71") + bump = GemBump.new("rubocop", "0.71", fake_bundler_specs) expected = "The gem *rubocop* was updated to version *0.71.0* and your TODO is now ready to be addressed." - bump.stub(:spec_set, fake_bundler_specs) do - assert_equal(expected, bump.met?) - end + assert_equal(expected, bump.met?) end def test_with_pessimistic_constraint - bump = GemBump.new("rubocop", "~>0.50") + bump = GemBump.new("rubocop", "~>0.50", fake_bundler_specs) expected = "The gem *rubocop* was updated to version *0.71.0* and your TODO is now ready to be addressed." - bump.stub(:spec_set, fake_bundler_specs) do - assert_equal(expected, bump.met?) - end + assert_equal(expected, bump.met?) end def test_with_multiple_constraints - bump = GemBump.new("rubocop", ["> 0.50", "< 1"]) + bump = GemBump.new("rubocop", ["> 0.50", "< 1"], fake_bundler_specs) expected = "The gem *rubocop* was updated to version *0.71.0* and your TODO is now ready to be addressed." - bump.stub(:spec_set, fake_bundler_specs) do - assert_equal(expected, bump.met?) - end + assert_equal(expected, bump.met?) end def test_when_gem_is_not_yet_released - bump = GemBump.new("rubocop", "1") + bump = GemBump.new("rubocop", "1", fake_bundler_specs) - bump.stub(:spec_set, fake_bundler_specs) do - assert_equal(false, bump.met?) - end + assert_equal(false, bump.met?) end def test_when_gem_does_not_exist - bump = GemBump.new("beep_boop", "1") + bump = GemBump.new("beep_boop", "1", fake_bundler_specs) expected = "The gem *beep_boop* is not in your dependencies, I can't determine if your TODO is ready to be addressed." - bump.stub(:spec_set, fake_bundler_specs) do - assert_equal(expected, bump.met?) - end + assert_equal(expected, bump.met?) end def fake_bundler_specs diff --git a/test/smart_todo/events/gem_release_test.rb b/test/smart_todo/events/gem_release_test.rb index 4970d18..c46fa9c 100644 --- a/test/smart_todo/events/gem_release_test.rb +++ b/test/smart_todo/events/gem_release_test.rb @@ -3,7 +3,7 @@ require "test_helper" module SmartTodo - module Events + class Events class GemReleaseTest < Minitest::Test def test_when_gem_is_released stub_request(:get, /rubygems.org/) diff --git a/test/smart_todo/events/issue_close_test.rb b/test/smart_todo/events/issue_close_test.rb index f94f98b..f438d9e 100644 --- a/test/smart_todo/events/issue_close_test.rb +++ b/test/smart_todo/events/issue_close_test.rb @@ -3,7 +3,7 @@ require "test_helper" module SmartTodo - module Events + class Events class IssueCloseTest < Minitest::Test def test_when_pull_request_is_close stub_request(:get, /api.github.com/) diff --git a/test/smart_todo/events/ruby_version_test.rb b/test/smart_todo/events/ruby_version_test.rb index 371c289..25f52c1 100644 --- a/test/smart_todo/events/ruby_version_test.rb +++ b/test/smart_todo/events/ruby_version_test.rb @@ -4,66 +4,54 @@ require "bundler" module SmartTodo - module Events + class Events class RubyVersionTest < Minitest::Test def test_when_a_single_ruby_version_is_met requirements = "2.5.7" - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("2.5.7")) expectation = "The currently installed version of Ruby 2.5.7 is = 2.5.7." - ruby_version.stub(:installed_ruby_version, "2.5.7") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end def test_when_a_ruby_version_range_is_met requirements = [">= 2.5", "< 3"] - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("2.5.7")) expectation = "The currently installed version of Ruby 2.5.7 is >= 2.5, < 3." - ruby_version.stub(:installed_ruby_version, "2.5.7") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end def test_when_a_pessimistic_ruby_version_is_met requirements = "~> 2.5" - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("2.7.3")) expectation = "The currently installed version of Ruby 2.7.3 is ~> 2.5." - ruby_version.stub(:installed_ruby_version, "2.7.3") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end def test_when_a_single_ruby_version_is_not_met requirements = "2.5.7" - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("2.5.6")) expectation = false - ruby_version.stub(:installed_ruby_version, "2.5.6") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end def test_when_a_ruby_version_range_is_not_met requirements = [">= 2.5", "< 3"] - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("3.2.1")) expectation = false - ruby_version.stub(:installed_ruby_version, "3.2.1") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end def test_when_a_pessimistic_ruby_version_is_not_met requirements = "~> 2.5" - ruby_version = RubyVersion.new(requirements) + ruby_version = RubyVersion.new(requirements, Gem::Version.new("3.2.1")) expectation = false - ruby_version.stub(:installed_ruby_version, "3.2.1") do - assert_equal(expectation, ruby_version.met?) - end + assert_equal(expectation, ruby_version.met?) end end end diff --git a/test/smart_todo/parser/metadata_parser_test.rb b/test/smart_todo/metadata_parser_test.rb similarity index 59% rename from test/smart_todo/parser/metadata_parser_test.rb rename to test/smart_todo/metadata_parser_test.rb index ed13645..1d778e5 100644 --- a/test/smart_todo/parser/metadata_parser_test.rb +++ b/test/smart_todo/metadata_parser_test.rb @@ -7,88 +7,88 @@ module Parser class MetadataParserTest < Minitest::Test def test_parse_todo_metadata_with_one_event ruby_code = <<~RUBY - TODO(on: date('2019-08-04'), to: 'john@example.com') + # TODO(on: date('2019-08-04'), to: 'john@example.com') RUBY - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) assert_equal(1, result.events.size) - assert_equal("date", result.events[0].method_name) + assert_equal(:date, result.events[0].method_name) assert_equal(["john@example.com"], result.assignees) end def test_parse_todo_metadata_with_multiple_event ruby_code = <<~RUBY - TODO(on: date('2019-08-04'), on: gem_release('v1.2'), to: 'john@example.com') + # TODO(on: date('2019-08-04'), on: gem_release('v1.2'), to: 'john@example.com') RUBY - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) assert_equal(2, result.events.size) - assert_equal("date", result.events[0].method_name) - assert_equal("gem_release", result.events[1].method_name) + assert_equal(:date, result.events[0].method_name) + assert_equal(:gem_release, result.events[1].method_name) assert_equal(["john@example.com"], result.assignees) end def test_parse_todo_metadata_with_no_assignee ruby_code = <<~RUBY - TODO(on: date('2019-08-04')) + # TODO(on: date('2019-08-04')) RUBY - result = MetadataParser.parse(ruby_code) - assert_equal("date", result.events[0].method_name) + result = Todo.new(ruby_code) + assert_equal(:date, result.events[0].method_name) assert_empty(result.assignees) end def test_parse_todo_metadata_with_multiple_assignees ruby_code = <<~RUBY - TODO(on: something('abc', '123', '456'), to: 'john@example.com', to: 'janne@example.com') + # TODO(on: something('abc', '123', '456'), to: 'john@example.com', to: 'janne@example.com') RUBY - result = MetadataParser.parse(ruby_code) - assert_equal("something", result.events[0].method_name) + result = Todo.new(ruby_code) + assert_equal(:something, result.events[0].method_name) assert_equal(["abc", "123", "456"], result.events[0].arguments) assert_equal(["john@example.com", "janne@example.com"], result.assignees) end def test_parse_todo_metadata_with_repeated_assignees ruby_code = <<~RUBY - TODO(on: something('abc', '123', '456'), to: 'john@example.com', to: 'john@example.com') + # TODO(on: something('abc', '123', '456'), to: 'john@example.com', to: 'john@example.com') RUBY - result = MetadataParser.parse(ruby_code) - assert_equal("something", result.events[0].method_name) + result = Todo.new(ruby_code) + assert_equal(:something, result.events[0].method_name) assert_equal(["abc", "123", "456"], result.events[0].arguments) assert_equal(["john@example.com", "john@example.com"], result.assignees) end def test_parse_todo_metadata_with_multiple_arguments ruby_code = <<~RUBY - TODO(on: something('abc', '123', '456'), to: 'john@example.com') + # TODO(on: something('abc', '123', '456'), to: 'john@example.com') RUBY - result = MetadataParser.parse(ruby_code) - assert_equal("something", result.events[0].method_name) + result = Todo.new(ruby_code) + assert_equal(:something, result.events[0].method_name) assert_equal(["abc", "123", "456"], result.events[0].arguments) assert_equal(["john@example.com"], result.assignees) end def test_parse_when_todo_metadata_is_uncorrectly_formatted ruby_code = <<~RUBY - TODO(foo: 'bar', lol: 'ahah') + # TODO(foo: 'bar', lol: 'ahah') RUBY - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) assert_empty(result.events) assert_empty(result.assignees) end def test_parse_when_todo_metadata_on_is_uncorrectly_formatted ruby_code = <<~RUBY - TODO(on: '2019-08-04') + # TODO(on: '2019-08-04') RUBY - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) - assert_equal(["Incorrect `:on` event format: 2019-08-04"], result.errors) + assert_equal(["Incorrect `:on` event format: \"2019-08-04\""], result.errors) end def test_when_a_smart_todo_has_incorrect_ruby_syntax @@ -99,17 +99,17 @@ def hello end EOM - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) assert_empty(result.events) assert_empty(result.assignees) end def test_parse_when_todo_metadata_is_not_ruby_code ruby_code = <<~RUBY - TODO: Do this when done + # TODO: Do this when done RUBY - result = MetadataParser.parse(ruby_code) + result = Todo.new(ruby_code) assert_empty(result.events) assert_empty(result.assignees) end diff --git a/test/smart_todo/parser/comment_parser_test.rb b/test/smart_todo/parser/comment_parser_test.rb deleted file mode 100644 index 3ca5b0a..0000000 --- a/test/smart_todo/parser/comment_parser_test.rb +++ /dev/null @@ -1,169 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -module SmartTodo - module Parser - class CommentParserTest < Minitest::Test - def test_parse_one_todo_with_single_line_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Remove this code once done - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal("Remove this code once done\n", todo[0].comment) - end - - def test_parse_multiple_todo_with_single_line_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Remove this code once done - def hello - end - - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Remove this code once done - def bar - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(2, todo.size) - assert_equal("Remove this code once done\n", todo[0].comment) - assert_equal("Remove this code once done\n", todo[1].comment) - end - - def test_parse_one_todo_with_multi_line_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Remove this code once done - # This is important - # Please don't disappoint me - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal(<<~EOM, todo[0].comment) - Remove this code once done - This is important - Please don't disappoint me - EOM - end - - def test_parse_multiple_todo_with_multi_line_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Remove this code once done - # This is important - # Please don't disappoint me - def hello - end - - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Hello World - # Good Bye! - def bar - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(2, todo.size) - assert_equal(<<~EOM, todo[0].comment) - Remove this code once done - This is important - Please don't disappoint me - EOM - assert_equal(<<~EOM, todo[1].comment) - Hello World - Good Bye! - EOM - end - - def test_parse_no_todo - ruby_code = <<~RUBY - # This is a regular comment - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_empty(todo) - end - - def test_parse_todo_with_no_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal("", todo[0].comment) - end - - def test_parse_todo_with_unindented_comment - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # Oups comment is not indented to the TODO - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal("", todo[0].comment) - end - - def test_parse_todo_with_weird_comment_indentation - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - #bla - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal("", todo[0].comment) - end - - def test_parse_todo_with_nothing_else - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - # The rest of the file is completely empty - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal(1, todo.size) - assert_equal("The rest of the file is completely empty\n", todo[0].comment) - end - - def test_parse_no_comment_at_all - ruby_code = <<~RUBY - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_empty(todo) - end - - def test_parse_todo_and_creates_metadata - ruby_code = <<~RUBY - # TODO(on: date('2019-08-04'), to: 'john@example.com') - def hello - end - RUBY - - todo = CommentParser.new(ruby_code).parse - assert_equal("date", todo[0].metadata.events[0].method_name) - assert_equal(["john@example.com"], todo[0].metadata.assignees) - end - end - end -end diff --git a/test/smart_todo/smart_todo_cop_test.rb b/test/smart_todo/smart_todo_cop_test.rb index fade793..6192a27 100644 --- a/test/smart_todo/smart_todo_cop_test.rb +++ b/test/smart_todo/smart_todo_cop_test.rb @@ -28,7 +28,7 @@ def hello def test_add_offense_when_todo_has_an_invalid_event expect_offense(<<~RUBY) # TODO(on: '2019-08-04', to: 'john@example.com') - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SmartTodo/SmartTodoCop: Invalid TODO format: Incorrect `:on` event format: 2019-08-04. For more info please look at https://github.com/Shopify/smart_todo/wiki/Syntax + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SmartTodo/SmartTodoCop: Invalid TODO format: Incorrect `:on` event format: "2019-08-04". For more info please look at https://github.com/Shopify/smart_todo/wiki/Syntax def hello end RUBY