checkoff 0.257.0 → 0.260.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/checkoff/attachments.rb +14 -15
  3. data/lib/checkoff/cli.rb +7 -12
  4. data/lib/checkoff/clients.rb +0 -2
  5. data/lib/checkoff/custom_fields.rb +13 -26
  6. data/lib/checkoff/internal/asana_event_enrichment.rb +11 -13
  7. data/lib/checkoff/internal/asana_event_filter.rb +1 -1
  8. data/lib/checkoff/internal/config_loader.rb +0 -2
  9. data/lib/checkoff/internal/logging.rb +0 -2
  10. data/lib/checkoff/internal/project_hashes.rb +5 -4
  11. data/lib/checkoff/internal/search_url/custom_field_param_converter.rb +9 -9
  12. data/lib/checkoff/internal/search_url/custom_field_variant.rb +7 -0
  13. data/lib/checkoff/internal/search_url/date_param_converter.rb +13 -4
  14. data/lib/checkoff/internal/search_url/parser.rb +2 -7
  15. data/lib/checkoff/internal/search_url/simple_param_converter.rb +11 -5
  16. data/lib/checkoff/internal/section_selector_evaluator.rb +1 -1
  17. data/lib/checkoff/internal/selector_classes/common/function_evaluator.rb +5 -2
  18. data/lib/checkoff/internal/selector_classes/common.rb +13 -31
  19. data/lib/checkoff/internal/selector_classes/function_evaluator.rb +20 -4
  20. data/lib/checkoff/internal/selector_classes/project/function_evaluator.rb +11 -2
  21. data/lib/checkoff/internal/selector_classes/project.rb +5 -13
  22. data/lib/checkoff/internal/selector_classes/section/function_evaluator.rb +8 -5
  23. data/lib/checkoff/internal/selector_classes/section.rb +2 -5
  24. data/lib/checkoff/internal/selector_classes/task/function_evaluator.rb +2 -2
  25. data/lib/checkoff/internal/selector_classes/task.rb +24 -23
  26. data/lib/checkoff/internal/selector_evaluator.rb +8 -9
  27. data/lib/checkoff/internal/task_hashes.rb +17 -9
  28. data/lib/checkoff/internal/task_timing.rb +0 -4
  29. data/lib/checkoff/internal/thread_local.rb +0 -2
  30. data/lib/checkoff/monkeypatches/resource_marshalling.rb +3 -3
  31. data/lib/checkoff/my_tasks.rb +1 -4
  32. data/lib/checkoff/portfolios.rb +0 -1
  33. data/lib/checkoff/project_selectors.rb +1 -1
  34. data/lib/checkoff/projects.rb +19 -8
  35. data/lib/checkoff/resources.rb +14 -3
  36. data/lib/checkoff/sections.rb +13 -19
  37. data/lib/checkoff/subtasks.rb +1 -2
  38. data/lib/checkoff/tags.rb +3 -4
  39. data/lib/checkoff/task_searches.rb +1 -8
  40. data/lib/checkoff/task_selectors.rb +25 -12
  41. data/lib/checkoff/tasks.rb +8 -16
  42. data/lib/checkoff/timelines.rb +5 -10
  43. data/lib/checkoff/timing.rb +1 -2
  44. data/lib/checkoff/version.rb +1 -1
  45. data/lib/checkoff/workspaces.rb +1 -7
  46. data/rbi/checkoff.rbi +122 -179
  47. data/sig/checkoff.rbs +83 -181
  48. metadata +2 -2
@@ -105,12 +105,9 @@ module Checkoff
105
105
  # @param custom_field_name [String]
106
106
  # @return [String, nil]
107
107
  def evaluate(resource, custom_field_name)
108
- # @sg-ignore
109
- custom_field = @custom_fields.resource_custom_field_by_name(resource, custom_field_name)
110
- # @sg-ignore
108
+ custom_field = custom_fields.resource_custom_field_by_name(resource, custom_field_name)
111
109
  return nil if custom_field.nil?
112
110
 
113
- # @sg-ignore
114
111
  custom_field['display_value']
115
112
  end
116
113
  end
@@ -125,17 +122,13 @@ module Checkoff
125
122
  false
126
123
  end
127
124
 
128
- # @sg-ignore
129
125
  # @param resource [Asana::Resources::Task,Asana::Resources::Project]
130
- # @sg-ignore
131
126
  # @param custom_field_gid [String]
132
127
  # @return [String, nil]
133
- # @sg-ignore
134
128
  def evaluate(resource, custom_field_gid)
135
- # @sg-ignore
136
- custom_field = @custom_fields.resource_custom_field_by_gid_or_raise(resource, custom_field_gid)
137
- # @sg-ignore
138
- custom_field['display_value']
129
+ custom_field = custom_fields.resource_custom_field_by_gid_or_raise(resource, custom_field_gid)
130
+ display_value = custom_field['display_value']
131
+ display_value.nil? ? nil : T.cast(display_value, String)
139
132
  end
140
133
  end
141
134
 
@@ -152,17 +145,13 @@ module Checkoff
152
145
  end
153
146
 
154
147
  # @param resource [Asana::Resources::Task,Asana::Resources::Project]
155
- # @sg-ignore
156
148
  # @param custom_field_gid [String]
157
149
  # @param custom_field_values_gids [Array<String>]
158
150
  # @return [Boolean]
159
- # @sg-ignore
160
151
  def evaluate(resource, custom_field_gid, custom_field_values_gids)
161
- # @sg-ignore
162
- actual_custom_field_values_gids = @custom_fields.resource_custom_field_values_gids_or_raise(resource,
163
- custom_field_gid)
152
+ actual_custom_field_values_gids = custom_fields.resource_custom_field_values_gids_or_raise(resource,
153
+ custom_field_gid)
164
154
 
165
- # @sg-ignore
166
155
  actual_custom_field_values_gids.intersect?(custom_field_values_gids)
167
156
  end
168
157
  end
@@ -181,17 +170,13 @@ module Checkoff
181
170
 
182
171
  # @param resource [Asana::Resources::Task,Asana::Resources::Project]
183
172
  # @param custom_field_name [String]
184
- # @sg-ignore
185
173
  # @param custom_field_value_names [Array<String>]
186
- # @sg-ignore
187
174
  # @return [Boolean]
188
175
  def evaluate(resource, custom_field_name, custom_field_value_names)
189
176
  actual_custom_field_values_names =
190
- # @sg-ignore
191
- @custom_fields.resource_custom_field_values_names_by_name(resource,
192
- custom_field_name)
177
+ custom_fields.resource_custom_field_values_names_by_name(resource,
178
+ custom_field_name)
193
179
 
194
- # @sg-ignore
195
180
  actual_custom_field_values_names.intersect?(custom_field_value_names)
196
181
  end
197
182
  end
@@ -214,12 +199,10 @@ module Checkoff
214
199
  # @return [Boolean]
215
200
  def evaluate(resource, custom_field_gid, custom_field_values_gids)
216
201
  actual_custom_field_values_gids =
217
- # @sg-ignore
218
- @custom_fields.resource_custom_field_values_gids_or_raise(resource,
219
- custom_field_gid)
202
+ custom_fields.resource_custom_field_values_gids_or_raise(resource,
203
+ custom_field_gid)
220
204
 
221
205
  custom_field_values_gids.all? do |custom_field_value|
222
- # @sg-ignore
223
206
  actual_custom_field_values_gids.include?(custom_field_value)
224
207
  end
225
208
  end
@@ -240,8 +223,6 @@ module Checkoff
240
223
  # @param resource [Asana::Resources::Task, Asana::Resources::Project]
241
224
  # @param prefix [String]
242
225
  # @return [boolish]
243
- # @sg-ignore Checkoff::SelectorClasses::Common::NameStartsWithPFunctionEvaluator#evaluate
244
- # return type could not be inferred
245
226
  def evaluate(resource, prefix)
246
227
  resource.name&.start_with?(prefix)
247
228
  end
@@ -255,9 +236,10 @@ module Checkoff
255
236
 
256
237
  # @param _resource [Asana::Resources::Task,Asana::Resources::Project]
257
238
  # @return [String]
258
- # @sg-ignore
259
239
  def evaluate(_resource)
260
- T.cast(selector, String)
240
+ raise "Expected a String selector, got #{selector.inspect}" unless selector.is_a?(String)
241
+
242
+ selector
261
243
  end
262
244
  end
263
245
  end
@@ -9,13 +9,23 @@ module Checkoff
9
9
  class FunctionEvaluator
10
10
  include Logging
11
11
 
12
+ # Concrete subclasses each declare their own initialize with their
13
+ # specific keyword args (and set @selector themselves before calling
14
+ # super()); this signature exists only so Solargraph can resolve
15
+ # `evaluator_class.new(selector:, **kwargs)` call sites that hold a
16
+ # `Class<FunctionEvaluator>` reference rather than a concrete subclass -
17
+ # deliberately a no-op so a bare `super()` call from those subclasses
18
+ # can't clobber the @selector they already set.
19
+ # @param selector [Symbol, Array, String, nil]
20
+ # @param _kwargs [Hash]
21
+ def initialize(selector: nil, **_kwargs); end
22
+
12
23
  # @param _index [Integer]
13
24
  def evaluate_arg?(_index)
14
25
  true
15
26
  end
16
27
 
17
28
  # @return [Boolean]
18
- # @sg-ignore return type could not be inferred — abstract method body is only `raise`
19
29
  def matches?
20
30
  raise 'Override me!'
21
31
  end
@@ -23,17 +33,23 @@ module Checkoff
23
33
  # @param _task [Asana::Resources::Task]
24
34
  # @param _args [Array<Object>]
25
35
  # @return [Object]
26
- # @sg-ignore return type could not be inferred — abstract method body is only `raise`
27
36
  def evaluate(_task, *_args)
28
37
  raise 'Implement me!'
29
38
  end
30
39
 
31
40
  private
32
41
 
33
- # @param object [Object]
42
+ # @param object [Array<Symbol, Array>, Symbol]
34
43
  # @param fn_name [Symbol]
44
+ # @return [Boolean]
35
45
  def fn?(object, fn_name)
36
- object.is_a?(Array) && !object.empty? && [fn_name, fn_name.to_s].include?(object[0])
46
+ if object.is_a?(Array) && !object.empty?
47
+ # @type [Array<Symbol, Array>]
48
+ array = object
49
+ [fn_name, fn_name.to_s].include?(array[0])
50
+ else
51
+ false
52
+ end
37
53
  end
38
54
  end
39
55
  end
@@ -8,7 +8,7 @@ module Checkoff
8
8
  module Project
9
9
  # Base class to evaluate a project selector function given fully evaluated arguments
10
10
  class FunctionEvaluator < ::Checkoff::SelectorClasses::FunctionEvaluator
11
- # @param selector [Array(Symbol, Array), String]
11
+ # @param selector [Array<Symbol, Array>, Symbol]
12
12
  # @param projects [Checkoff::Projects]
13
13
  # @param portfolios [Checkoff::Portfolios]
14
14
  # @param workspaces [Checkoff::Workspaces]
@@ -25,9 +25,18 @@ module Checkoff
25
25
  super()
26
26
  end
27
27
 
28
+ # @return [Checkoff::Projects]
29
+ attr_reader :projects
30
+
31
+ # @return [Checkoff::Portfolios]
32
+ attr_reader :portfolios
33
+
34
+ # @return [Checkoff::Workspaces]
35
+ attr_reader :workspaces
36
+
28
37
  private
29
38
 
30
- # @return [Array(Symbol, Array)]
39
+ # @return [Array<Symbol, Array>, Symbol]
31
40
  attr_reader :selector
32
41
  end
33
42
  end
@@ -17,7 +17,6 @@ module Checkoff
17
17
 
18
18
  # @param resource [Asana::Resources::Project]
19
19
  # @return [String, nil]
20
- # @sg-ignore
21
20
  def evaluate(resource)
22
21
  resource.due_date
23
22
  end
@@ -33,11 +32,9 @@ module Checkoff
33
32
 
34
33
  # @param project [Asana::Resources::Project]
35
34
  # @param period [Symbol] - :now_or_before or :this_week
36
- # @sg-ignore
37
35
  # @return [Boolean]
38
36
  def evaluate(project, period = :now_or_before)
39
- # @sg-ignore
40
- @projects.project_ready?(project, period:)
37
+ projects.project_ready?(project, period:)
41
38
  end
42
39
  end
43
40
 
@@ -53,19 +50,14 @@ module Checkoff
53
50
  # @param portfolio_name [String]
54
51
  # @param workspace_name [String, nil]
55
52
  # @param extra_project_fields [Array<String>]
56
- # @sg-ignore
57
53
  #
58
- # @sg-ignore
59
54
  # @return [Boolean]
60
55
  def evaluate(project, portfolio_name, workspace_name: nil, extra_project_fields: [])
61
56
  workspace_name ||= project.workspace&.name
62
- # @sg-ignore
63
- workspace_name ||= @workspaces.default_workspace.name
64
- # @sg-ignore
65
- projects = @portfolios.projects_in_portfolio(workspace_name, portfolio_name,
66
- extra_project_fields:)
67
- # @sg-ignore
68
- projects.any? { |p| p.name == project.name }
57
+ workspace_name ||= workspaces.default_workspace.name
58
+ matching_projects = portfolios.projects_in_portfolio(T.must(workspace_name), portfolio_name,
59
+ extra_project_fields:)
60
+ matching_projects.any? { |p| p.name == project.name }
69
61
  end
70
62
  end
71
63
  end
@@ -8,7 +8,7 @@ module Checkoff
8
8
  module Section
9
9
  # Base class to evaluate a project selector function given fully evaluated arguments
10
10
  class FunctionEvaluator < ::Checkoff::SelectorClasses::FunctionEvaluator
11
- # @param selector [Array(Symbol, Array), String]
11
+ # @param selector [Array<Symbol, Array>, Symbol]
12
12
  # @param client [Asana::Client]
13
13
  # @param sections [Checkoff::Sections]
14
14
  # @param kwargs [Hash]
@@ -22,13 +22,16 @@ module Checkoff
22
22
  super()
23
23
  end
24
24
 
25
- private
26
-
27
- # @return [Array(Symbol, Array)]
28
- attr_reader :selector
25
+ # @return [Checkoff::Sections]
26
+ attr_reader :sections
29
27
 
30
28
  # @return [Asana::Client]
31
29
  attr_reader :client
30
+
31
+ private
32
+
33
+ # @return [Array<Symbol, Array>, Symbol]
34
+ attr_reader :selector
32
35
  end
33
36
  end
34
37
  end
@@ -18,12 +18,11 @@ module Checkoff
18
18
  # @param section [Asana::Resources::Section]
19
19
  #
20
20
  # @return [Boolean]
21
- # @sg-ignore
22
21
  def evaluate(section)
23
22
  tasks = client.tasks.get_tasks(section: section.gid,
24
23
  per_page: 100,
25
24
  options: { fields: ['resource_subtype'] })
26
- T.unsafe(tasks).last&.resource_subtype == 'milestone'
25
+ tasks.last&.resource_subtype == 'milestone'
27
26
  end
28
27
  end
29
28
 
@@ -37,11 +36,9 @@ module Checkoff
37
36
 
38
37
  # @param section [Asana::Resources::Section]
39
38
  #
40
- # @sg-ignore
41
39
  # @return [Boolean]
42
40
  def evaluate(section)
43
- # @sg-ignore
44
- @sections.tasks_by_section_gid(section.gid).any?
41
+ sections.tasks_by_section_gid(section.gid).any?
45
42
  end
46
43
  end
47
44
  end
@@ -8,7 +8,7 @@ module Checkoff
8
8
  module Task
9
9
  # Base class to evaluate a task selector function given fully evaluated arguments
10
10
  class FunctionEvaluator < ::Checkoff::SelectorClasses::FunctionEvaluator
11
- # @param selector [Array(Symbol, Array), String]
11
+ # @param selector [Array<Symbol, Array>, Symbol]
12
12
  # @param tasks [Checkoff::Tasks]
13
13
  # @param timelines [Checkoff::Timelines]
14
14
  # @param custom_fields [Checkoff::CustomFields]
@@ -36,7 +36,7 @@ module Checkoff
36
36
 
37
37
  private
38
38
 
39
- # @return [Array(Symbol, Array)]
39
+ # @return [Array<Symbol, Array>, Symbol]
40
40
  attr_reader :selector
41
41
  end
42
42
  end
@@ -45,10 +45,14 @@ module Checkoff
45
45
  # @param task [Asana::Resources::Task]
46
46
  # @param section_name_prefix [String]
47
47
  # @return [Boolean]
48
- # @sg-ignore
49
48
  def evaluate(task, section_name_prefix)
49
+ # @type [Hash{'unwrapped' => Hash}]
50
50
  task_data = tasks.task_to_h(task)
51
- task_data.fetch('unwrapped').fetch('membership_by_section_name').keys.any? do |section_name|
51
+ # @type [Hash{'membership_by_section_name' => Hash}]
52
+ unwrapped = task_data.fetch('unwrapped')
53
+ # @type [Array]
54
+ section_names = unwrapped.fetch('membership_by_section_name').keys
55
+ section_names.any? do |section_name|
52
56
  String(section_name).start_with?(section_name_prefix)
53
57
  end
54
58
  end
@@ -65,13 +69,17 @@ module Checkoff
65
69
  false
66
70
  end
67
71
 
68
- # @sg-ignore
69
72
  # @param task [Asana::Resources::Task]
70
73
  # @param section_name [String]
71
74
  # @return [Boolean]
72
75
  def evaluate(task, section_name)
76
+ # @type [Hash{'unwrapped' => Hash}]
73
77
  task_data = tasks.task_to_h(task)
74
- task_data.fetch('unwrapped').fetch('membership_by_section_name').keys.any?(section_name)
78
+ # @type [Hash{'membership_by_section_name' => Hash}]
79
+ unwrapped = task_data.fetch('unwrapped')
80
+ # @type [Array]
81
+ section_names = unwrapped.fetch('membership_by_section_name').keys
82
+ section_names.any?(section_name)
75
83
  end
76
84
  end
77
85
 
@@ -89,13 +97,11 @@ module Checkoff
89
97
  # @param task [Asana::Resources::Task]
90
98
  # @param project_name [String]
91
99
  # @return [Boolean]
92
- # @sg-ignore
93
100
  def evaluate(task, project_name)
94
101
  project_names = task.memberships.map do |membership|
95
- m = T.cast(membership, T::Hash[String, T.untyped])
96
- T.cast(m.fetch('project'), T::Hash[String, T.untyped]).fetch('name')
102
+ membership.fetch('project').fetch('name')
97
103
  end
98
- T.cast(project_names.include?(project_name), T::Boolean)
104
+ project_names.include?(project_name)
99
105
  end
100
106
  end
101
107
 
@@ -132,9 +138,11 @@ module Checkoff
132
138
  # @param task [Asana::Resources::Task]
133
139
  # @param tag_name [String]
134
140
  # @return [Boolean]
135
- # @sg-ignore
136
141
  def evaluate(task, tag_name)
137
- task.tags.map(&:name).include? tag_name
142
+ # @type [Array<Asana::Resources::Tag>]
143
+ tags = task.tags.to_a
144
+ tag_names = tags.map(&:name)
145
+ tag_names.include?(tag_name)
138
146
  end
139
147
  end
140
148
 
@@ -190,10 +198,8 @@ module Checkoff
190
198
 
191
199
  # @param task [Asana::Resources::Task]
192
200
  # @return [Boolean]
193
- # @sg-ignore Checkoff::SelectorClasses::Task::UnassignedPFunctionEvaluator#evaluate
194
- # return type could not be inferred
195
201
  def evaluate(task)
196
- T.cast(task.assignee.nil? == true, T::Boolean)
202
+ task.assignee.nil?
197
203
  end
198
204
  end
199
205
 
@@ -207,10 +213,8 @@ module Checkoff
207
213
 
208
214
  # @param task [Asana::Resources::Task]
209
215
  # @return [Boolean]
210
- # @sg-ignore Checkoff::SelectorClasses::Task::DueDateSetPFunctionEvaluator#evaluate
211
- # return type could not be inferred
212
216
  def evaluate(task)
213
- T.cast(!(task.due_at.nil? && task.due_on.nil?), T::Boolean)
217
+ !(task.due_at.nil? && task.due_on.nil?)
214
218
  end
215
219
  end
216
220
 
@@ -235,13 +239,11 @@ module Checkoff
235
239
 
236
240
  # @type [Array<Asana::Resources::Story>]
237
241
  stories = task.stories(per_page: 100).to_a.reject do |story|
238
- # @sg-ignore
239
242
  excluding_resource_subtypes.include? story.resource_subtype
240
243
  end
241
244
  return true if stories.empty? # no stories == infinitely old!
242
245
 
243
- last_story = stories.last
244
- # @sg-ignore
246
+ last_story = T.must(stories.last)
245
247
  last_story_created_at = Time.parse(last_story.created_at)
246
248
  n_days_ago = Time.at(Time.now.to_i - (num_days * 86_400))
247
249
  last_story_created_at < n_days_ago
@@ -362,12 +364,11 @@ module Checkoff
362
364
  # @param task [Asana::Resources::Task]
363
365
  #
364
366
  # @return [Boolean]
365
- # @sg-ignore Checkoff::SelectorClasses::Task::MilestonePFunctionEvaluator#evaluate
366
- # return type could not be inferred
367
367
  def evaluate(task)
368
- raise 'Please add resource_subtype to extra_fields' if task.resource_subtype.nil?
368
+ resource_subtype = task.resource_subtype
369
+ raise 'Please add resource_subtype to extra_fields' if resource_subtype.nil?
369
370
 
370
- task.resource_subtype == 'milestone'
371
+ resource_subtype == 'milestone'
371
372
  end
372
373
  end
373
374
 
@@ -1,23 +1,21 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Checkoff
5
5
  # Base class to evaluate Asana resource selectors against an Asana resource
6
6
  class SelectorEvaluator
7
- # @param selector [Symbol, Array<Symbol, Integer, Array>]
7
+ # @param selector [Symbol, Array<Symbol, String, Integer, Array>]
8
8
  # @return [Boolean, Object, Array, nil]
9
9
  def evaluate(selector)
10
10
  return true if selector.empty?
11
11
 
12
12
  function_evaluators.each do |evaluator_class|
13
13
  # @type [Checkoff::SelectorClasses::FunctionEvaluator]
14
- # @sg-ignore
15
14
  evaluator = evaluator_class.new(selector:,
16
15
  **initializer_kwargs)
17
16
 
18
17
  next unless evaluator.matches?
19
18
 
20
- # @sg-ignore
21
19
  return try_this_evaluator(selector, evaluator)
22
20
  end
23
21
 
@@ -32,19 +30,20 @@ module Checkoff
32
30
  end
33
31
 
34
32
  # @return [Array<Class<Checkoff::SelectorClasses::FunctionEvaluator>>]
35
- # @sg-ignore
36
33
  def function_evaluators
37
34
  raise 'Implement me!'
38
35
  end
39
36
 
40
- # @param selector [Array]
37
+ # @param selector [Symbol, Array]
41
38
  # @param evaluator [Checkoff::SelectorClasses::FunctionEvaluator]
42
39
  # @return [Array]
43
40
  def evaluate_args(selector, evaluator)
44
41
  return [] unless selector.is_a?(Array)
45
42
 
46
- # @sg-ignore
47
- selector[1..].map.with_index do |item, index|
43
+ rest = selector[1..]
44
+ return [] unless rest
45
+
46
+ rest.map.with_index do |item, index|
48
47
  if evaluator.evaluate_arg?(index)
49
48
  evaluate(item)
50
49
  else
@@ -53,7 +52,7 @@ module Checkoff
53
52
  end
54
53
  end
55
54
 
56
- # @param selector [Array]
55
+ # @param selector [Symbol, Array]
57
56
  # @param evaluator [Checkoff::SelectorClasses::FunctionEvaluator]
58
57
  # @return [Boolean, Object, nil]
59
58
  def try_this_evaluator(selector, evaluator)
@@ -9,7 +9,7 @@ module Checkoff
9
9
  # @param task [Asana::Resources::Task]
10
10
  # @return [Hash]
11
11
  def task_to_h(task)
12
- # @type [Hash]
12
+ # @type [Hash{String => Object}]
13
13
  task_hash = task.to_h
14
14
  task_hash['unwrapped'] = {}
15
15
  unwrap_custom_fields(task_hash)
@@ -18,7 +18,7 @@ module Checkoff
18
18
  task_hash
19
19
  end
20
20
 
21
- # @param task_data [Hash]
21
+ # @param task_data [Hash{String => Object}]
22
22
  # @param client [Asana::Client]
23
23
  #
24
24
  # @return [Asana::Resources::Task]
@@ -34,7 +34,7 @@ module Checkoff
34
34
  # @param task_hash [Hash]
35
35
  # @return [void]
36
36
  def unwrap_custom_fields(task_hash)
37
- # @type [Array<Hash>,nil]
37
+ # @type [Array<Hash{String => Object}>,nil]
38
38
  custom_fields = task_hash.fetch('custom_fields', nil)
39
39
 
40
40
  return if custom_fields.nil?
@@ -53,8 +53,7 @@ module Checkoff
53
53
  return unless task_hash.key? 'assignee_section'
54
54
 
55
55
  assignee_section = task_hash.fetch('assignee_section')
56
- # @type [Hash{String => String}]
57
- assignee = T.cast(task_hash.fetch('assignee'), T::Hash[String, String])
56
+ assignee = assignee_hash(task_hash)
58
57
  memberships << {
59
58
  'section' => assignee_section.dup,
60
59
  'project' => {
@@ -64,24 +63,33 @@ module Checkoff
64
63
  }
65
64
  end
66
65
 
67
- # @param task_hash [Hash]
66
+ # @param task_hash [Hash{String => String, Hash, Array}]
67
+ # @return [Hash{String => Object}]
68
+ def assignee_hash(task_hash)
69
+ assignee = task_hash.fetch('assignee')
70
+ raise "Expected assignee to be a Hash, got #{assignee.class}" unless assignee.is_a?(Hash)
71
+
72
+ assignee
73
+ end
74
+
75
+ # @param task_hash [Hash{String => Object}]
68
76
  # @param resource [String]
69
77
  # @param memberships [Array<Hash>]
70
78
  # @param key [String]
71
79
  #
72
80
  # @return [void]
73
81
  def unwrap_memberships(task_hash, memberships, resource, key)
74
- # @type [Hash]
82
+ # @type [Hash{String => Object}]
75
83
  unwrapped = task_hash.fetch('unwrapped')
76
84
  unwrapped["membership_by_#{resource}_#{key}"] = memberships.group_by do |membership|
77
85
  membership[resource][key]
78
86
  end.transform_values(&:first)
79
87
  end
80
88
 
81
- # @param task_hash [Hash]
89
+ # @param task_hash [Hash{String => Object}]
82
90
  # @return [void]
83
91
  def unwrap_all_memberships(task_hash)
84
- # @type [Array<Hash>]
92
+ # @type [Array<Hash{String => Object}>]
85
93
  memberships = task_hash.fetch('memberships', []).dup
86
94
  add_user_task_list(task_hash, memberships)
87
95
  unwrap_memberships(task_hash, memberships, 'section', 'gid')
@@ -19,16 +19,12 @@ module Checkoff
19
19
 
20
20
  # @param task [Asana::Resources::Task]
21
21
  # @return [Time, nil]
22
- # @sg-ignore return type could not be inferred — #to_time called via &. on a Date|Time
23
- # union receiver
24
22
  def start_time(task)
25
23
  date_or_time_field_by_name(task, :start)&.to_time
26
24
  end
27
25
 
28
26
  # @param task [Asana::Resources::Task]
29
27
  # @return [Time, nil]
30
- # @sg-ignore return type could not be inferred — #to_time called via &. on a Date|Time
31
- # union receiver
32
28
  def due_time(task)
33
29
  date_or_time_field_by_name(task, :due)&.to_time
34
30
  end
@@ -10,8 +10,6 @@ module Checkoff
10
10
  # @param value [Object,Boolean]
11
11
  # @yieldreturn [generic<T>]
12
12
  # @return [generic<T>]
13
- # @sg-ignore return type could not be inferred — Solargraph's generic/yield-return
14
- # inference gap for #with_thread_local_variable
15
13
  def with_thread_local_variable(name, value, &block)
16
14
  old_value = Thread.current[name]
17
15
  Thread.current[name] = value
@@ -20,19 +20,19 @@ module Asana
20
20
  # Public: The base resource class which provides some sugar over common
21
21
  # resource functionality.
22
22
  class Resource
23
- # @return [Hash]
23
+ # @return [Hash{String => Object}]
24
24
  def marshal_dump
25
25
  { 'data' => @_data,
26
26
  'client' => @_client }
27
27
  end
28
28
 
29
- # @param data [Hash]
29
+ # @param data [Hash{String => Object}]
30
30
  #
31
31
  # @return [void]
32
32
  def marshal_load(data)
33
33
  # @type [Asana::Client]
34
34
  @_client = data.fetch('client')
35
- # @type [Hash]
35
+ # @type [Hash{String => Object}]
36
36
  @_data = data.fetch('data')
37
37
  @_data.each do |k, v|
38
38
  if respond_to?(k)
@@ -67,10 +67,7 @@ module Checkoff
67
67
  sections.each_entry { |section| by_section[section_key(section.name)] = [] }
68
68
  tasks.each do |task|
69
69
  assignee_section = task.assignee_section
70
- # @sg-ignore assignee_section can be nil (task has no My Tasks section) but is
71
- # dereferenced without a nil guard — looks like a real gap, flagging for the
72
- # follow-up code PR
73
- current_section = section_key(assignee_section.name)
70
+ current_section = section_key(assignee_section&.name)
74
71
  by_section[current_section] ||= []
75
72
  by_section[current_section] << task
76
73
  end
@@ -47,7 +47,6 @@ module Checkoff
47
47
  # @param portfolio_name [String]
48
48
  #
49
49
  # @return [Asana::Resources::Portfolio]
50
- # @sg-ignore nil check above is not flow-sensitive
51
50
  def portfolio_or_raise(workspace_name, portfolio_name)
52
51
  portfolio_obj = portfolio(workspace_name, portfolio_name)
53
52
  raise "Could not find portfolio #{portfolio_name} under workspace #{workspace_name}." if portfolio_obj.nil?
@@ -42,7 +42,7 @@ module Checkoff
42
42
  end
43
43
 
44
44
  # @param [Asana::Resources::Project] project
45
- # @param [Array(Symbol, Array)] project_selector Filter based on
45
+ # @param [Array<Symbol, String, Array>] project_selector Filter based on
46
46
  # project details. Examples: [:tag, 'foo'] [:not, [:tag, 'foo']] [:tag, 'foo']
47
47
  # @return [Boolean]
48
48
  def filter_via_project_selector(project, project_selector)