smart_todo 1.10.0 → 1.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eed299ced3a12723ccf8cb775ea33b3ce784b1c05c18ced890b3c8b184c0124e
4
- data.tar.gz: a2e1ff5973fb8b1b7b40ee700a56acbee6c6c30df0c23e16e8c9505fe24af8ba
3
+ metadata.gz: cd76c16d18b12ed3caada5a661031dd5f14c60aa1b0b227c0eb720116755c2cc
4
+ data.tar.gz: 91dadbb687dcfd22c4152f2b38054873f6d64fce3b43d8d96b8ac6891947e92a
5
5
  SHA512:
6
- metadata.gz: a36e6f20daabc1e3cdd74d8c5b63fd6284e0581de67b769dbeea5c97948f438cddefae166cf8c260d11237e6fbb22beeab9e0da1b0c069277f10c7d4ac1468ba
7
- data.tar.gz: e020e58592618dd6567d8031dfe4c5c8c77bc7ccb372d7de2060c98f13ef072eaf31a01e16d2e9bc07118ea8553b2d716c95df19a0aed105b45baea1c6bf2406
6
+ metadata.gz: cede44f989ddbc2a4174c5c273e1e7e08521bbaaddc2a18fe5afc88d04fb84cdb6644e9d6c6839f6e7257dc1271f7f188bbdc6edec21a19debb8e0a438c20130
7
+ data.tar.gz: fc3d770921d2c2d7a275236ea068c9aef5f2202de990c644afeb4cfb974581e62e8ba77ae91bc6d6af265b438dde88b62052a9046f57301808a1a695d12e3c1c
@@ -15,7 +15,7 @@ jobs:
15
15
  version: [3.0, 3.1, 3.2, 3.3]
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v4
18
+ - uses: actions/checkout@v5
19
19
  - name: Set up Ruby ${{ matrix.version }}
20
20
  uses: ruby/setup-ruby@v1
21
21
  with:
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
 
9
9
  steps:
10
- - uses: actions/checkout@v4
10
+ - uses: actions/checkout@v5
11
11
  - name: Set up Ruby
12
12
  uses: ruby/setup-ruby@v1
13
13
  with:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_todo (1.10.0)
4
+ smart_todo (1.11.0)
5
5
  prism (~> 1.0)
6
6
 
7
7
  GEM
@@ -10,26 +10,26 @@ GEM
10
10
  addressable (2.8.7)
11
11
  public_suffix (>= 2.0.2, < 7.0)
12
12
  ast (2.4.3)
13
- bigdecimal (3.1.9)
14
- crack (1.0.0)
13
+ bigdecimal (3.3.1)
14
+ crack (1.0.1)
15
15
  bigdecimal
16
16
  rexml
17
- hashdiff (1.1.2)
17
+ hashdiff (1.2.1)
18
18
  json (2.10.2)
19
19
  language_server-protocol (3.17.0.4)
20
20
  lint_roller (1.1.0)
21
- minitest (5.25.5)
21
+ minitest (5.26.0)
22
22
  parallel (1.26.3)
23
23
  parser (3.3.7.3)
24
24
  ast (~> 2.4.1)
25
25
  racc
26
- prism (1.4.0)
27
- public_suffix (6.0.1)
26
+ prism (1.6.0)
27
+ public_suffix (6.0.2)
28
28
  racc (1.8.1)
29
29
  rainbow (3.1.1)
30
- rake (13.2.1)
30
+ rake (13.3.0)
31
31
  regexp_parser (2.10.0)
32
- rexml (3.4.1)
32
+ rexml (3.4.4)
33
33
  rubocop (1.74.0)
34
34
  json (~> 2.3)
35
35
  language_server-protocol (~> 3.17.0.2)
@@ -49,7 +49,7 @@ GEM
49
49
  unicode-display_width (3.1.4)
50
50
  unicode-emoji (~> 4.0, >= 4.0.4)
51
51
  unicode-emoji (4.0.4)
52
- webmock (3.25.1)
52
+ webmock (3.26.0)
53
53
  addressable (>= 2.8.0)
54
54
  crack (>= 0.3.2)
55
55
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -106,12 +106,40 @@ module SmartTodo
106
106
  end
107
107
 
108
108
  @errors.concat(todo.errors)
109
- dispatches << [event_message, todo] if event_met
109
+
110
+ next unless event_met
111
+
112
+ event_message = append_context_if_applicable(event_message, todo, event_met, events)
113
+
114
+ dispatches << [event_message, todo]
110
115
  end
111
116
 
112
117
  dispatches
113
118
  end
114
119
 
120
+ private
121
+
122
+ # @param event_message [String] the original event message
123
+ # @param todo [Todo] the todo object that may contain context
124
+ # @param event [Event] the event that was met
125
+ # @param events [Events] the events instance for fetching issue context
126
+ # @return [String] the event message, potentially with context appended
127
+ def append_context_if_applicable(event_message, todo, event, events)
128
+ return event_message unless should_apply_context?(todo, event)
129
+
130
+ org, repo, issue_number = todo.context.arguments
131
+ context_message = events.issue_context(org, repo, issue_number)
132
+
133
+ context_message ? "#{event_message}\n\n#{context_message}" : event_message
134
+ end
135
+
136
+ # @param todo [Todo] the todo object to check for context
137
+ # @param event [Event] the event to check
138
+ # @return [Boolean] true if context should be applied, false otherwise
139
+ def should_apply_context?(todo, event)
140
+ !!todo.context
141
+ end
142
+
115
143
  def process_dispatches(dispatches)
116
144
  queue = Queue.new
117
145
  dispatches.each { |dispatch| queue << dispatch }
@@ -75,6 +75,8 @@ module SmartTodo
75
75
  false
76
76
  end
77
77
  end
78
+ rescue Net::HTTPError, JSON::ParserError
79
+ "Error retrieving gem information for *#{gem_name}*."
78
80
  end
79
81
 
80
82
  # Check if +gem_name+ was bumped to the +requirements+ expected
@@ -130,6 +132,8 @@ module SmartTodo
130
132
  else
131
133
  false
132
134
  end
135
+ rescue Net::HTTPError, JSON::ParserError
136
+ "Error retrieving issue information for #{organization}/#{repo}##{issue_number}."
133
137
  end
134
138
 
135
139
  # Check if the pull request +pr_number+ is closed
@@ -155,6 +159,34 @@ module SmartTodo
155
159
  else
156
160
  false
157
161
  end
162
+ rescue Net::HTTPError, JSON::ParserError
163
+ "Error retrieving pull request information for #{organization}/#{repo}##{pr_number}."
164
+ end
165
+
166
+ # Retrieve context information for an issue
167
+ # This is used when a TODO has a context: issue() attribute
168
+ #
169
+ # @param organization [String] the GitHub organization name
170
+ # @param repo [String] the GitHub repo name
171
+ # @param issue_number [String, Integer]
172
+ # @return [String, nil]
173
+ def issue_context(organization, repo, issue_number)
174
+ headers = github_headers(organization, repo)
175
+ response = github_client.get("/repos/#{organization}/#{repo}/issues/#{issue_number}", headers)
176
+
177
+ if response.code_type < Net::HTTPClientError
178
+ nil
179
+ else
180
+ issue = JSON.parse(response.body)
181
+ state = issue["state"]
182
+ title = issue["title"]
183
+ assignee = issue["assignee"] ? "@#{issue["assignee"]["login"]}" : "unassigned"
184
+
185
+ "📌 Context: Issue ##{issue_number} - \"#{title}\" [#{state}] (#{assignee}) - " \
186
+ "https://github.com/#{organization}/#{repo}/issues/#{issue_number}"
187
+ end
188
+ rescue Net::HTTPError, JSON::ParserError
189
+ nil
158
190
  end
159
191
 
160
192
  # Check if the installed ruby version meets requirements.
@@ -4,6 +4,7 @@ module SmartTodo
4
4
  class Todo
5
5
  attr_reader :filepath, :comment, :indent
6
6
  attr_reader :events, :assignees, :errors
7
+ attr_accessor :context
7
8
 
8
9
  def initialize(source, filepath = "-e")
9
10
  @filepath = filepath
@@ -12,6 +13,7 @@ module SmartTodo
12
13
 
13
14
  @events = []
14
15
  @assignees = []
16
+ @context = nil
15
17
  @errors = []
16
18
 
17
19
  parse(source[(indent + 1)..])
@@ -66,6 +68,28 @@ module SmartTodo
66
68
  end
67
69
  when :to
68
70
  metadata.assignees << visit(element.value)
71
+ when :context
72
+ value = visit(element.value)
73
+
74
+ unless value.is_a?(String)
75
+ metadata.errors << "Incorrect `:context` format: expected string value"
76
+ next
77
+ end
78
+
79
+ unless value =~ %r{^([^/]+)/([^#]+)#(\d+)$}
80
+ metadata.errors << "Incorrect `:context` format: expected \"org/repo#issue_number\""
81
+ next
82
+ end
83
+
84
+ org = ::Regexp.last_match(1)
85
+ repo = ::Regexp.last_match(2)
86
+ issue_number = ::Regexp.last_match(3)
87
+
88
+ if org.empty? || repo.empty?
89
+ metadata.errors << "Incorrect `:context` format: org and repo cannot be empty"
90
+ else
91
+ metadata.context = CallNode.new(:issue, [org, repo, issue_number], element.value.location)
92
+ end
69
93
  end
70
94
  end
71
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmartTodo
4
- VERSION = "1.10.0"
4
+ VERSION = "1.11.0"
5
5
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "smart_todo"
4
+ require "parser"
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module SmartTodo
9
+ # A RuboCop cop to enforce proper formatting of SmartTodo comments.
10
+ # SmartTodo comments must have their description on separate lines, indented by 2 spaces.
11
+ #
12
+ # Bad:
13
+ # # TODO(on: date('2024-03-29'), to: 'john@example.com'): Remove this
14
+ # # TODO(on: date('2024-03-29'), to: 'john@example.com') Remove this
15
+ # # TODO(on: date('2024-03-29'), to: 'john@example.com')
16
+ # # Remove this (not indented)
17
+ #
18
+ # Good:
19
+ # # TODO(on: date('2024-03-29'), to: 'john@example.com')
20
+ # # Remove this (indented by 2 extra spaces)
21
+ #
22
+ class SmartTodoCommentFormatCop < Base
23
+ extend AutoCorrector
24
+
25
+ MSG_INLINE = "SmartTodo comment must not be on the same line as the TODO. " \
26
+ "For more info please look at https://github.com/Shopify/smart_todo/wiki/Syntax"
27
+ MSG_INDENT = "SmartTodo continuation line must be indented by 2 spaces. " \
28
+ "For more info please look at https://github.com/Shopify/smart_todo/wiki/Syntax"
29
+
30
+ SMART_TODO_PATTERN = /\A\s*#\s*(TODO|FIXME|OPTIMIZE)\(on:/
31
+ INLINE_TEXT_PATTERN = /\A(\s*#\s*(?:TODO|FIXME|OPTIMIZE)\(.+\))\s*:?\s+(.+)/
32
+
33
+ def on_new_investigation
34
+ processed_source.comments.each_with_index do |comment, index|
35
+ next unless smart_todo_comment?(comment)
36
+
37
+ check_inline_text(comment)
38
+ check_continuation_indent(comment, processed_source.comments[index + 1])
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def smart_todo_comment?(comment)
45
+ comment.text.match?(SMART_TODO_PATTERN)
46
+ end
47
+
48
+ def check_inline_text(comment)
49
+ match = comment.text.match(INLINE_TEXT_PATTERN)
50
+ return unless match
51
+
52
+ add_offense(comment.location.expression, message: MSG_INLINE) do |corrector|
53
+ todo_part = match[1]
54
+ text_part = match[2]
55
+ indentation = " " * comment.location.column
56
+
57
+ corrected = "#{todo_part}\n#{indentation}# #{text_part}"
58
+ corrector.replace(comment.location.expression, corrected)
59
+ end
60
+ end
61
+
62
+ def check_continuation_indent(comment, next_comment)
63
+ return unless next_comment
64
+ return unless next_comment.location.line == comment.location.line + 1
65
+ return if smart_todo_comment?(next_comment)
66
+ return if empty_comment?(next_comment)
67
+ return if properly_indented?(next_comment)
68
+
69
+ add_offense(next_comment.location.expression, message: MSG_INDENT) do |corrector|
70
+ corrected = fix_indentation(next_comment.text)
71
+ corrector.replace(next_comment.location.expression, corrected)
72
+ end
73
+ end
74
+
75
+ def empty_comment?(comment)
76
+ comment.text.match?(/\A\s*#\s*\z/)
77
+ end
78
+
79
+ def properly_indented?(comment)
80
+ # A properly indented continuation has exactly 2 spaces after the #
81
+ comment.text.match?(/\A\s*# \S/)
82
+ end
83
+
84
+ def fix_indentation(text)
85
+ # Extract leading whitespace and content
86
+ match = text.match(/\A(\s*)#\s*(\S.*)\z/)
87
+ return text unless match
88
+
89
+ leading_space = match[1]
90
+ content = match[2]
91
+ "#{leading_space}# #{content}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_todo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: prism
@@ -121,6 +121,7 @@ files:
121
121
  - lib/smart_todo/slack_client.rb
122
122
  - lib/smart_todo/todo.rb
123
123
  - lib/smart_todo/version.rb
124
+ - lib/smart_todo_comment_format_cop.rb
124
125
  - lib/smart_todo_cop.rb
125
126
  - service.yml
126
127
  - shipit.rubygems.yml
@@ -147,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
148
  - !ruby/object:Gem::Version
148
149
  version: '0'
149
150
  requirements: []
150
- rubygems_version: 3.6.6
151
+ rubygems_version: 3.7.2
151
152
  specification_version: 4
152
153
  summary: Enhance todo's comments in your codebase.
153
154
  test_files: []