smart_todo 1.6.0 → 1.8.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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmartTodo
4
- VERSION = "1.6.0"
4
+ VERSION = "1.8.0"
5
5
  end
data/lib/smart_todo.rb CHANGED
@@ -1,25 +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
- 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
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
- autoload :RubyVersion, "smart_todo/events/ruby_version"
22
- end
10
+ autoload :Todo, "smart_todo/todo"
11
+ autoload :CommentParser, "smart_todo/comment_parser"
12
+ autoload :HttpClientBuilder, "smart_todo/http_client_builder"
23
13
 
24
14
  module Dispatchers
25
15
  autoload :Base, "smart_todo/dispatchers/base"
@@ -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/ =~ comment.text
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::Parser::MetadataParser.parse(comment.gsub(/^#/, ""))
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::Parser::MethodNode) } &&
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.respond_to?(method) }
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("rexml")
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.6.0
4
+ version: 1.8.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-07-18 00:00:00.000000000 Z
11
+ date: 2024-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rexml
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
@@ -97,6 +97,7 @@ files:
97
97
  - ".github/workflows/rubocop.yml"
98
98
  - ".gitignore"
99
99
  - ".rubocop.yml"
100
+ - ".ruby-version"
100
101
  - CODE_OF_CONDUCT.md
101
102
  - Gemfile
102
103
  - Gemfile.lock
@@ -104,25 +105,21 @@ files:
104
105
  - README.md
105
106
  - Rakefile
106
107
  - bin/console
108
+ - bin/profile
107
109
  - bin/rubocop
108
110
  - bin/setup
109
111
  - dev.yml
110
112
  - exe/smart_todo
111
113
  - lib/smart_todo.rb
112
114
  - lib/smart_todo/cli.rb
115
+ - lib/smart_todo/comment_parser.rb
113
116
  - lib/smart_todo/dispatchers/base.rb
114
117
  - lib/smart_todo/dispatchers/output.rb
115
118
  - lib/smart_todo/dispatchers/slack.rb
116
119
  - lib/smart_todo/events.rb
117
- - lib/smart_todo/events/date.rb
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/events/ruby_version.rb
122
- - lib/smart_todo/parser/comment_parser.rb
123
- - lib/smart_todo/parser/metadata_parser.rb
124
- - lib/smart_todo/parser/todo_node.rb
120
+ - lib/smart_todo/http_client_builder.rb
125
121
  - lib/smart_todo/slack_client.rb
122
+ - lib/smart_todo/todo.rb
126
123
  - lib/smart_todo/version.rb
127
124
  - lib/smart_todo_cop.rb
128
125
  - service.yml
@@ -151,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
148
  - !ruby/object:Gem::Version
152
149
  version: '0'
153
150
  requirements: []
154
- rubygems_version: 3.4.16
151
+ rubygems_version: 3.5.13
155
152
  signing_key:
156
153
  specification_version: 4
157
154
  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,113 +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
- #
15
- # You can also set a per-org or per-repo token by exporting more specific environment variables:
16
- # +SMART_TODO_GITHUB_TOKEN__<ORG>+ and +SMART_TODO_GITHUB_TOKEN__<ORG>__<REPO>+
17
- # The +<ORG>+ and +<REPO>+ parts should be uppercased and use underscores.
18
- # For example, +Shopify/my-repo+ would become +SMART_TODO_GITHUB_TOKEN__SHOPIFY__MY_REPO=...+.
19
- class IssueClose
20
- TOKEN_ENV = "SMART_TODO_GITHUB_TOKEN"
21
-
22
- # @param organization [String]
23
- # @param repo [String]
24
- # @param pr_number [String, Integer]
25
- def initialize(organization, repo, pr_number, type:)
26
- @url = "/repos/#{organization}/#{repo}/#{type}/#{pr_number}"
27
- @organization = organization
28
- @repo = repo
29
- @pr_number = pr_number
30
- end
31
-
32
- # @return [String, false]
33
- def met?
34
- response = client.get(@url, default_headers)
35
-
36
- if response.code_type < Net::HTTPClientError
37
- error_message
38
- elsif pull_request_closed?(response.body)
39
- message
40
- else
41
- false
42
- end
43
- end
44
-
45
- # Error message send to Slack in case the Pull Request or Issue couldn't be found.
46
- #
47
- # @return [String]
48
- def error_message
49
- <<~EOM
50
- I can't retrieve the information from the PR or Issue *#{@pr_number}* in the
51
- *#{@organization}/#{@repo}* repository.
52
-
53
- If the repository is a private one, make sure to export the `#{TOKEN_ENV}`
54
- environment variable with a correct GitHub token.
55
- EOM
56
- end
57
-
58
- # @return [String]
59
- def message
60
- <<~EOM
61
- The Pull Request or Issue https://github.com/#{@organization}/#{@repo}/pull/#{@pr_number}
62
- is now closed, your TODO is ready to be addressed.
63
- EOM
64
- end
65
-
66
- private
67
-
68
- # @return [Net::HTTP] an instance of Net::HTTP
69
- def client
70
- @client ||= Net::HTTP.new("api.github.com", Net::HTTP.https_default_port).tap do |client|
71
- client.use_ssl = true
72
- end
73
- end
74
-
75
- # @param pull_request [String] the Pull Request or Issue
76
- # detail sent back from the GitHub API
77
- #
78
- # @return [true, false]
79
- def pull_request_closed?(pull_request)
80
- JSON.parse(pull_request)["state"] == "closed"
81
- end
82
-
83
- # @return [Hash]
84
- def default_headers
85
- { "Accept" => "application/vnd.github.v3+json" }.tap do |headers|
86
- token = authorization_token
87
- headers["Authorization"] = "token #{token}" if token
88
- end
89
- end
90
-
91
- # @return [String, nil]
92
- def authorization_token
93
- # Will look in order for:
94
- # SMART_TODO_GITHUB_TOKEN__ORG__REPO
95
- # SMART_TODO_GITHUB_TOKEN__ORG
96
- # SMART_TODO_GITHUB_TOKEN
97
- parts = [
98
- TOKEN_ENV,
99
- @organization.upcase.gsub(/[^A-Z0-9]/, "_"),
100
- @repo.upcase.gsub(/[^A-Z0-9]/, "_"),
101
- ]
102
-
103
- (parts.size - 1).downto(0).each do |i|
104
- key = parts[0..i].join("__")
105
- token = ENV[key]
106
- return token unless token.nil? || token.empty?
107
- end
108
-
109
- nil
110
- end
111
- end
112
- end
113
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SmartTodo
4
- module Events
5
- # An event that checks the currently installed ruby version.
6
- # @example
7
- # RubyVersion.new(['>= 2.3', '< 3'])
8
- class RubyVersion
9
- def initialize(requirements)
10
- @requirements = Gem::Requirement.new(requirements)
11
- end
12
-
13
- # @param requirements [Array<String>] a list of version specifiers
14
- # @return [String, false]
15
- def met?
16
- if @requirements.satisfied_by?(Gem::Version.new(installed_ruby_version))
17
- message(installed_ruby_version)
18
- else
19
- false
20
- end
21
- end
22
-
23
- # @param installed_ruby_version [String], requirements [String]
24
- # @return [String]
25
- def message(installed_ruby_version)
26
- "The currently installed version of Ruby #{installed_ruby_version} is #{@requirements}."
27
- end
28
-
29
- private
30
-
31
- def installed_ruby_version
32
- RUBY_VERSION
33
- end
34
- end
35
- end
36
- 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