dependabot-github_actions 0.393.0 → 0.394.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: 4209eb988f4acc1fdb05b3f95694cfba5e7972ea3cd29020a5a32aca407b64ab
4
- data.tar.gz: 5638cd93a7068ed55e5eb77c5f8a85d3f5a0b01c7d3591dc3ef77c5f634498ff
3
+ metadata.gz: 21ec9924cfc31066fa2dccd2cf7ca50e9f7969efde8940d67adbd19443a2f46d
4
+ data.tar.gz: e98917f3e0def956dc94d9341c2a17a4c9739a517ebaec569b3bc766ba0a3014
5
5
  SHA512:
6
- metadata.gz: 93a1ad33067ffe60ffa89d0963fe22df5b949677e00184a798aa27f00cf71d6d86c513248c960459269097e89610cb4128565adb356ee9478053cd141ee7a799
7
- data.tar.gz: 8ee8ecae42b4e24b50ac8ae56482553479564564f04842326baf4ecb40557edecf6adc641667f75087184fa1ecda30533df78e591404efc37a13a2c06dd90583
6
+ metadata.gz: f16959b6407974932411f006a734f63cdae7279624a63ffe8a956521535688d030dce0b1ec588f9c611060060377f4dad024599d617257e5be386eeaf1154919
7
+ data.tar.gz: 647815b9aae2a57c32f60edf7c7c03e8095227fe561769beeb9c96ed7ff97ef6d2ef905d112e377a181f797ba9a06f06a793acb03cf8830e29cd896a0c1e3704
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
@@ -11,6 +11,7 @@ require "dependabot/file_parsers/base"
11
11
  require "dependabot/github_actions/constants"
12
12
  require "dependabot/github_actions/version"
13
13
  require "dependabot/github_actions/package_manager"
14
+ require "dependabot/github_actions/workflow_file"
14
15
 
15
16
  # For docs, see
16
17
  # https://help.github.com/en/articles/configuring-a-workflow#referencing-actions-in-your-workflow
@@ -54,62 +55,106 @@ module Dependabot
54
55
  sig { params(file: Dependabot::DependencyFile).returns(Dependabot::FileParsers::Base::DependencySet) }
55
56
  def workfile_file_dependencies(file)
56
57
  dependency_set = DependencySet.new
57
-
58
- json = YAML.safe_load(T.must(file.content), aliases: true, permitted_classes: [Date, Time, Symbol])
59
- return dependency_set if json.nil?
60
-
61
- uses_strings = deep_fetch_uses(json.fetch("jobs", json.fetch("runs", nil))).uniq
62
-
63
- uses_strings.each do |string|
64
- # TODO: Support Docker references and path references
65
- next if string.start_with?(".", "docker://")
66
- next unless string.match?(GITHUB_REPO_REFERENCE)
67
-
68
- dep = build_github_dependency(file, string)
69
- git_checker = Dependabot::GitCommitChecker.new(
70
- dependency: dep,
71
- credentials: credentials,
72
- consider_version_branches_pinned: true
73
- )
74
- if git_checker.git_repo_reachable?
75
- next unless git_checker.pinned?
76
-
77
- # If dep does not have an assigned (semver) version, look for a commit that references a semver tag
78
- unless dep.version
79
- resolved = git_checker.version_for_pinned_sha
80
-
81
- if resolved
82
- dep = Dependency.new(
83
- name: dep.name,
84
- version: resolved.to_s,
85
- requirements: dep.requirements,
86
- package_manager: dep.package_manager
87
- )
88
- end
89
- end
90
- end
58
+ workflow_file = WorkflowFile.new(T.must(file.content))
59
+ workflow_file.uses_declarations.group_by(&:value).each do |string, declarations|
60
+ dep = dependency_for_declarations(file, workflow_file, string, declarations)
61
+ next unless dep
91
62
 
92
63
  dependency_set << dep
93
64
  end
94
-
95
65
  dependency_set
96
66
  rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
97
67
  raise Dependabot::DependencyFileNotParseable, file.path
98
68
  end
99
69
 
100
- sig { params(file: Dependabot::DependencyFile, string: String).returns(Dependabot::Dependency) }
101
- def build_github_dependency(file, string)
70
+ sig do
71
+ params(
72
+ file: Dependabot::DependencyFile,
73
+ workflow_file: WorkflowFile,
74
+ string: String,
75
+ declarations: T::Array[WorkflowFile::UsesDeclaration]
76
+ ).returns(T.nilable(Dependabot::Dependency))
77
+ end
78
+ def dependency_for_declarations(file, workflow_file, string, declarations)
79
+ # TODO: Support Docker references and path references
80
+ return if string.start_with?(".", "docker://")
81
+ return unless string.match?(GITHUB_REPO_REFERENCE)
82
+
83
+ metadata = declarations.map do |declaration|
84
+ declaration_metadata(workflow_file, declaration)
85
+ end
86
+ dep = build_github_dependency(file, string, metadata)
87
+ git_checker = Dependabot::GitCommitChecker.new(
88
+ dependency: dep,
89
+ credentials: credentials,
90
+ consider_version_branches_pinned: true
91
+ )
92
+ return dep unless git_checker.git_repo_reachable?
93
+ return unless git_checker.pinned?
94
+
95
+ dependency_with_resolved_version(dep, git_checker) || dep
96
+ end
97
+
98
+ sig do
99
+ params(
100
+ workflow_file: WorkflowFile,
101
+ declaration: WorkflowFile::UsesDeclaration
102
+ ).returns(T::Hash[Symbol, Object])
103
+ end
104
+ def declaration_metadata(workflow_file, declaration)
105
+ metadata = T.let({ declaration_string: declaration.value }, T::Hash[Symbol, Object])
106
+ if workflow_file.source_metadata_required?(declaration)
107
+ metadata[:yaml_source] = workflow_file.source_metadata(declaration)
108
+ end
109
+ metadata
110
+ end
111
+
112
+ sig do
113
+ params(
114
+ dep: Dependabot::Dependency,
115
+ git_checker: Dependabot::GitCommitChecker
116
+ ).returns(T.nilable(Dependabot::Dependency))
117
+ end
118
+ def dependency_with_resolved_version(dep, git_checker)
119
+ return if dep.version
120
+
121
+ resolved = git_checker.version_for_pinned_sha
122
+ return unless resolved
123
+
124
+ Dependency.new(
125
+ name: dep.name,
126
+ version: resolved.to_s,
127
+ requirements: dep.requirements,
128
+ package_manager: dep.package_manager
129
+ )
130
+ end
131
+
132
+ sig do
133
+ params(
134
+ file: Dependabot::DependencyFile,
135
+ string: String,
136
+ metadata: T::Array[T::Hash[Symbol, Object]]
137
+ ).returns(Dependabot::Dependency)
138
+ end
139
+ def build_github_dependency(file, string, metadata)
102
140
  unless source&.hostname == GITHUB_COM
103
- dep = github_dependency(file, string, T.must(source).hostname)
141
+ dep = github_dependency(file, string, T.must(source).hostname, metadata)
104
142
  git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials)
105
143
  return dep if git_checker.git_repo_reachable?
106
144
  end
107
145
 
108
- github_dependency(file, string, GITHUB_COM)
146
+ github_dependency(file, string, GITHUB_COM, metadata)
109
147
  end
110
148
 
111
- sig { params(file: Dependabot::DependencyFile, string: String, hostname: String).returns(Dependabot::Dependency) }
112
- def github_dependency(file, string, hostname)
149
+ sig do
150
+ params(
151
+ file: Dependabot::DependencyFile,
152
+ string: String,
153
+ hostname: String,
154
+ metadata: T::Array[T::Hash[Symbol, Object]]
155
+ ).returns(Dependabot::Dependency)
156
+ end
157
+ def github_dependency(file, string, hostname, metadata)
113
158
  details = T.must(string.match(GITHUB_REPO_REFERENCE)).named_captures
114
159
  repo_name = "#{details.fetch(OWNER_KEY)}/#{details.fetch(REPO_KEY)}"
115
160
  path = details[PATH_KEY]
@@ -130,45 +175,24 @@ module Dependabot
130
175
  Dependency.new(
131
176
  name: name,
132
177
  version: version,
133
- requirements: [{
134
- requirement: nil,
135
- groups: [],
136
- source: {
137
- type: "git",
138
- url: "https://#{hostname}/#{repo_name}".downcase,
139
- ref: ref,
140
- branch: nil
141
- },
142
- file: file.name,
143
- metadata: { declaration_string: string }
144
- }],
178
+ requirements: metadata.map do |details|
179
+ {
180
+ requirement: nil,
181
+ groups: [],
182
+ source: {
183
+ type: "git",
184
+ url: "https://#{hostname}/#{repo_name}".downcase,
185
+ ref: ref,
186
+ branch: nil
187
+ },
188
+ file: file.name,
189
+ metadata: details
190
+ }
191
+ end,
145
192
  package_manager: PackageManager::NAME
146
193
  )
147
194
  end
148
195
 
149
- sig { params(json_obj: T.untyped, found_uses: T::Array[String]).returns(T::Array[String]) }
150
- def deep_fetch_uses(json_obj, found_uses = [])
151
- case json_obj
152
- when Hash then deep_fetch_uses_from_hash(json_obj, found_uses)
153
- when Array then json_obj.flat_map { |o| deep_fetch_uses(o, found_uses) }
154
- else []
155
- end
156
- end
157
-
158
- sig { params(json_object: T::Hash[String, T.untyped], found_uses: T::Array[String]).returns(T::Array[String]) }
159
- def deep_fetch_uses_from_hash(json_object, found_uses)
160
- if json_object.key?(USES_KEY)
161
- found_uses << json_object[USES_KEY]
162
- elsif json_object.key?(STEPS_KEY)
163
- # Bypass other fields as uses are under steps if they exist
164
- deep_fetch_uses(json_object[STEPS_KEY], found_uses)
165
- else
166
- json_object.values.flat_map { |obj| deep_fetch_uses(obj, found_uses) }
167
- end
168
-
169
- found_uses
170
- end
171
-
172
196
  sig { returns(T::Array[Dependabot::DependencyFile]) }
173
197
  def workflow_files
174
198
  dependency_files.reject { |file| file.path.delete_prefix("/") == LOCKFILE_PATH }
@@ -0,0 +1,244 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/github_actions/file_updater/workflow_updater"
5
+
6
+ module Dependabot
7
+ module GithubActions
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkflowUpdater
10
+ class FlowSequenceComments
11
+ extend T::Sig
12
+
13
+ class ItemComments < T::Struct
14
+ extend T::Sig
15
+
16
+ const :trailing, T.nilable(String)
17
+ const :standalone, T::Array[String]
18
+
19
+ sig { returns(T::Boolean) }
20
+ def any?
21
+ !trailing.nil? || standalone.any?
22
+ end
23
+ end
24
+
25
+ sig do
26
+ params(
27
+ workflow_file: WorkflowFile,
28
+ sequence: Psych::Nodes::Sequence,
29
+ commenter: VersionCommenter,
30
+ comment_locator: SourceCommentLocator,
31
+ item_indent: String
32
+ ).void
33
+ end
34
+ def initialize(workflow_file:, sequence:, commenter:, comment_locator:, item_indent:)
35
+ @workflow_file = workflow_file
36
+ @sequence = sequence
37
+ @commenter = commenter
38
+ @comment_locator = comment_locator
39
+ @item_indent = item_indent
40
+ @leading_comments = T.let(build_leading_comments, T::Array[String])
41
+ @source_comments = T.let(build_source_comments, T::Hash[Integer, ItemComments])
42
+ end
43
+
44
+ sig { params(index: Integer, updates: T::Array[SourceUpdate]).returns(ItemComments) }
45
+ def rendered_for(index, updates)
46
+ source_comments = @source_comments.fetch(index, ItemComments.new(trailing: nil, standalone: []))
47
+ return generated_comments(updates) unless source_comments.any?
48
+
49
+ ItemComments.new(
50
+ trailing: process_comment(source_comments.trailing, updates, trailing: true),
51
+ standalone: source_comments.standalone.filter_map do |comment|
52
+ process_comment(comment, updates, trailing: false)
53
+ end
54
+ )
55
+ end
56
+
57
+ sig do
58
+ params(
59
+ index: Integer,
60
+ item_count: Integer,
61
+ source: String,
62
+ updates: T::Array[SourceUpdate]
63
+ ).returns(String)
64
+ end
65
+ def render_item(index:, item_count:, source:, updates:)
66
+ comments = rendered_for(index, updates)
67
+ comma = index == item_count - 1 ? "" : ","
68
+ line = "#{item_indent}#{source}#{comma}#{comments.trailing}"
69
+ standalone = comments.standalone.map { |comment| "#{item_indent}#{comment}" }
70
+ ([line] + standalone).join(workflow_file.newline)
71
+ end
72
+
73
+ sig { returns(T::Array[String]) }
74
+ def leading_lines
75
+ @leading_comments.map { |comment| "#{item_indent}#{comment}" }
76
+ end
77
+
78
+ sig { params(updates: T::Array[SourceUpdate]).returns(T.nilable(ContentEdit)) }
79
+ def claim_trailing_comment(updates)
80
+ located_comment = comment_locator.for_sequence(sequence)
81
+ return unless located_comment
82
+
83
+ update = updates.find do |candidate|
84
+ commenter.recognized_comment?(
85
+ located_comment.comment,
86
+ candidate.old_ref,
87
+ candidate.new_ref
88
+ )
89
+ end
90
+ return unless update
91
+ return unless update.declaration.sequence_item_index
92
+
93
+ add_source_comment(T.must(update.declaration.sequence_item_index), located_comment.comment)
94
+ ContentEdit.new(
95
+ start_offset: located_comment.start_offset,
96
+ end_offset: located_comment.end_offset,
97
+ replacement: ""
98
+ )
99
+ end
100
+
101
+ private
102
+
103
+ sig { returns(WorkflowFile) }
104
+ attr_reader :workflow_file
105
+
106
+ sig { returns(Psych::Nodes::Sequence) }
107
+ attr_reader :sequence
108
+
109
+ sig { returns(VersionCommenter) }
110
+ attr_reader :commenter
111
+
112
+ sig { returns(SourceCommentLocator) }
113
+ attr_reader :comment_locator
114
+
115
+ sig { returns(String) }
116
+ attr_reader :item_indent
117
+
118
+ sig { returns(T::Hash[Integer, ItemComments]) }
119
+ def build_source_comments
120
+ children = workflow_file.node_children(sequence)
121
+ closing_offset = closing_bracket_offset
122
+
123
+ children.each_with_index.to_h do |child, index|
124
+ gap_end = children[index + 1] ? node_start_offset(T.must(children[index + 1])) : closing_offset
125
+ gap = workflow_file.content.byteslice(node_end_offset(child)...gap_end) || ""
126
+ [index, comments_from_gap(gap)]
127
+ end
128
+ end
129
+
130
+ sig { returns(T::Array[String]) }
131
+ def build_leading_comments
132
+ first_child = workflow_file.node_children(sequence).first
133
+ return [] unless first_child
134
+
135
+ gap = workflow_file.content.byteslice((opening_bracket_offset + 1)...node_start_offset(first_child)) || ""
136
+ gap.lines.filter_map { |line| find_comment(line, trailing: false) }
137
+ end
138
+
139
+ sig { params(gap: String).returns(ItemComments) }
140
+ def comments_from_gap(gap)
141
+ lines = gap.lines
142
+ first_line = lines.shift || gap
143
+ trailing = find_comment(first_line, trailing: true)
144
+ standalone = lines.filter_map { |line| find_comment(line, trailing: false) }
145
+
146
+ ItemComments.new(trailing: trailing, standalone: standalone)
147
+ end
148
+
149
+ sig { params(line: String, trailing: T::Boolean).returns(T.nilable(String)) }
150
+ def find_comment(line, trailing:)
151
+ body = line.delete_suffix("\n").delete_suffix("\r")
152
+ comment = comment_locator.find_line(body)
153
+ return unless comment
154
+
155
+ trailing ? " #{comment.lstrip}" : comment.lstrip
156
+ end
157
+
158
+ sig { params(index: Integer, comment: String).void }
159
+ def add_source_comment(index, comment)
160
+ existing = @source_comments.fetch(index, ItemComments.new(trailing: nil, standalone: []))
161
+ @source_comments[index] =
162
+ if existing.trailing
163
+ ItemComments.new(
164
+ trailing: existing.trailing,
165
+ standalone: existing.standalone + [comment.lstrip]
166
+ )
167
+ else
168
+ ItemComments.new(
169
+ trailing: " #{comment.lstrip}",
170
+ standalone: existing.standalone
171
+ )
172
+ end
173
+ end
174
+
175
+ sig do
176
+ params(
177
+ comment: T.nilable(String),
178
+ updates: T::Array[SourceUpdate],
179
+ trailing: T::Boolean
180
+ ).returns(T.nilable(String))
181
+ end
182
+ def process_comment(comment, updates, trailing:)
183
+ return unless comment
184
+
185
+ index = 0
186
+ while index < updates.length
187
+ update = T.must(updates[index])
188
+ updated = commenter.updated_comment(comment, update.old_ref, update.new_ref)
189
+ return normalize_comment(updated, trailing: trailing) if updated
190
+ return if commenter.recognized_comment?(comment, update.old_ref, update.new_ref)
191
+
192
+ index += 1
193
+ end
194
+
195
+ normalize_comment(comment, trailing: trailing)
196
+ end
197
+
198
+ sig { params(comment: String, trailing: T::Boolean).returns(String) }
199
+ def normalize_comment(comment, trailing:)
200
+ trailing ? " #{comment.lstrip}" : comment.lstrip
201
+ end
202
+
203
+ sig { params(updates: T::Array[SourceUpdate]).returns(ItemComments) }
204
+ def generated_comments(updates)
205
+ comments = updates.filter_map { |update| commenter.new_comment(update.old_ref, update.new_ref) }.uniq
206
+ raise "Conflicting version comments for one workflow step" if comments.length > 1
207
+
208
+ ItemComments.new(trailing: comments.first, standalone: [])
209
+ end
210
+
211
+ sig { returns(Integer) }
212
+ def opening_bracket_offset
213
+ source = workflow_file.node_source(sequence)
214
+ opening_bracket = T.must(source.b.index("["))
215
+ node_start_offset(sequence) + opening_bracket
216
+ end
217
+
218
+ sig { returns(Integer) }
219
+ def closing_bracket_offset
220
+ source = workflow_file.node_source(sequence)
221
+ closing_bracket = T.must(source.b.rindex("]"))
222
+ node_start_offset(sequence) + closing_bracket
223
+ end
224
+
225
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
226
+ def node_start_offset(node)
227
+ workflow_file.offset(
228
+ workflow_file.node_start_line(node),
229
+ workflow_file.node_start_column(node)
230
+ )
231
+ end
232
+
233
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
234
+ def node_end_offset(node)
235
+ workflow_file.offset(
236
+ workflow_file.node_end_line(node),
237
+ workflow_file.node_end_column(node)
238
+ )
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,147 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/github_actions/file_updater/workflow_updater"
5
+
6
+ module Dependabot
7
+ module GithubActions
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkflowUpdater
10
+ class SourceCommentLocator
11
+ extend T::Sig
12
+
13
+ class LocatedComment < T::Struct
14
+ const :comment, String
15
+ const :start_offset, Integer
16
+ const :end_offset, Integer
17
+ const :syntax_adjacent, T::Boolean
18
+ end
19
+
20
+ sig do
21
+ params(
22
+ workflow_file: WorkflowFile,
23
+ comment_finder: YamlCommentFinder,
24
+ metadata_builder: WorkflowFile::MetadataBuilder
25
+ ).void
26
+ end
27
+ def initialize(workflow_file:, comment_finder:, metadata_builder:)
28
+ @workflow_file = workflow_file
29
+ @comment_finder = comment_finder
30
+ @metadata_builder = metadata_builder
31
+ end
32
+
33
+ sig { params(declaration: WorkflowFile::UsesDeclaration).returns(T.nilable(LocatedComment)) }
34
+ def for_declaration(declaration)
35
+ anchor = metadata_builder.comment_anchor(declaration)
36
+ return unless anchor
37
+
38
+ locate(
39
+ line: anchor.first,
40
+ column: anchor.last,
41
+ owner_sequence: declaration.steps_sequence,
42
+ block_scalar: block_scalar?(declaration.source_node)
43
+ )
44
+ end
45
+
46
+ sig { params(sequence: Psych::Nodes::Sequence).returns(T.nilable(LocatedComment)) }
47
+ def for_sequence(sequence)
48
+ locate(
49
+ line: workflow_file.node_end_line(sequence),
50
+ column: workflow_file.node_end_column(sequence),
51
+ owner_sequence: sequence,
52
+ block_scalar: false
53
+ )
54
+ end
55
+
56
+ sig { params(source: String).returns(T.nilable(String)) }
57
+ def find_line(source)
58
+ comment_finder.find_line(source)
59
+ end
60
+
61
+ private
62
+
63
+ sig { returns(WorkflowFile) }
64
+ attr_reader :workflow_file
65
+
66
+ sig { returns(YamlCommentFinder) }
67
+ attr_reader :comment_finder
68
+
69
+ sig { returns(WorkflowFile::MetadataBuilder) }
70
+ attr_reader :metadata_builder
71
+
72
+ sig do
73
+ params(
74
+ line: Integer,
75
+ column: Integer,
76
+ owner_sequence: T.nilable(Psych::Nodes::Sequence),
77
+ block_scalar: T::Boolean
78
+ ).returns(T.nilable(LocatedComment))
79
+ end
80
+ def locate(line:, column:, owner_sequence:, block_scalar:)
81
+ suffix = workflow_file.line_body(line).byteslice(workflow_file.byte_column(line, column)..) || ""
82
+ comment = comment_finder.find(suffix)
83
+ return unless comment
84
+
85
+ relative_start = T.must(suffix.b.index(comment.b))
86
+ start_offset = workflow_file.offset(line, column) + relative_start
87
+ return if inside_other_sequence?(start_offset, owner_sequence)
88
+
89
+ prefix = suffix.byteslice(0...relative_start) || ""
90
+ LocatedComment.new(
91
+ comment: comment,
92
+ start_offset: start_offset,
93
+ end_offset: start_offset + comment.bytesize,
94
+ syntax_adjacent: adjacent_prefix?(prefix, block_scalar)
95
+ )
96
+ end
97
+
98
+ sig do
99
+ params(
100
+ offset: Integer,
101
+ owner_sequence: T.nilable(Psych::Nodes::Sequence)
102
+ ).returns(T::Boolean)
103
+ end
104
+ def inside_other_sequence?(offset, owner_sequence)
105
+ workflow_file.sequences.any? do |sequence|
106
+ next false if owner_sequence.equal?(sequence)
107
+
108
+ node_start_offset(sequence) <= offset && offset < node_end_offset(sequence)
109
+ end
110
+ end
111
+
112
+ sig { params(prefix: String, block_scalar: T::Boolean).returns(T::Boolean) }
113
+ def adjacent_prefix?(prefix, block_scalar)
114
+ return true if prefix.match?(/\A[ \t,\]\}]*\z/)
115
+ return false unless block_scalar
116
+
117
+ prefix.match?(/\A[>|0-9+-]+[ \t]*\z/)
118
+ end
119
+
120
+ sig { params(node: T.nilable(Psych::Nodes::Node)).returns(T::Boolean) }
121
+ def block_scalar?(node)
122
+ return false unless node.is_a?(Psych::Nodes::Scalar)
123
+
124
+ style = workflow_file.scalar_node_style(node)
125
+ [Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED].include?(style)
126
+ end
127
+
128
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
129
+ def node_start_offset(node)
130
+ workflow_file.offset(
131
+ workflow_file.node_start_line(node),
132
+ workflow_file.node_start_column(node)
133
+ )
134
+ end
135
+
136
+ sig { params(node: Psych::Nodes::Node).returns(Integer) }
137
+ def node_end_offset(node)
138
+ workflow_file.offset(
139
+ workflow_file.node_end_line(node),
140
+ workflow_file.node_end_column(node)
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end