dependabot-javascript 0.296.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/lib/dependabot/bun.rb +49 -0
  3. data/lib/dependabot/javascript/bun/file_fetcher.rb +77 -0
  4. data/lib/dependabot/javascript/bun/file_parser/bun_lock.rb +156 -0
  5. data/lib/dependabot/javascript/bun/file_parser/lockfile_parser.rb +55 -0
  6. data/lib/dependabot/javascript/bun/file_parser.rb +74 -0
  7. data/lib/dependabot/javascript/bun/file_updater/lockfile_updater.rb +138 -0
  8. data/lib/dependabot/javascript/bun/file_updater.rb +75 -0
  9. data/lib/dependabot/javascript/bun/helpers.rb +72 -0
  10. data/lib/dependabot/javascript/bun/package_manager.rb +48 -0
  11. data/lib/dependabot/javascript/bun/requirement.rb +11 -0
  12. data/lib/dependabot/javascript/bun/update_checker/conflicting_dependency_resolver.rb +64 -0
  13. data/lib/dependabot/javascript/bun/update_checker/dependency_files_builder.rb +47 -0
  14. data/lib/dependabot/javascript/bun/update_checker/latest_version_finder.rb +450 -0
  15. data/lib/dependabot/javascript/bun/update_checker/library_detector.rb +76 -0
  16. data/lib/dependabot/javascript/bun/update_checker/requirements_updater.rb +203 -0
  17. data/lib/dependabot/javascript/bun/update_checker/subdependency_version_resolver.rb +144 -0
  18. data/lib/dependabot/javascript/bun/update_checker/version_resolver.rb +525 -0
  19. data/lib/dependabot/javascript/bun/update_checker/vulnerability_auditor.rb +165 -0
  20. data/lib/dependabot/javascript/bun/update_checker.rb +440 -0
  21. data/lib/dependabot/javascript/bun/version.rb +11 -0
  22. data/lib/dependabot/javascript/shared/constraint_helper.rb +359 -0
  23. data/lib/dependabot/javascript/shared/dependency_files_filterer.rb +164 -0
  24. data/lib/dependabot/javascript/shared/file_fetcher.rb +283 -0
  25. data/lib/dependabot/javascript/shared/file_parser/lockfile_parser.rb +106 -0
  26. data/lib/dependabot/javascript/shared/file_parser.rb +454 -0
  27. data/lib/dependabot/javascript/shared/file_updater/npmrc_builder.rb +394 -0
  28. data/lib/dependabot/javascript/shared/file_updater/package_json_preparer.rb +87 -0
  29. data/lib/dependabot/javascript/shared/file_updater/package_json_updater.rb +376 -0
  30. data/lib/dependabot/javascript/shared/file_updater.rb +179 -0
  31. data/lib/dependabot/javascript/shared/language.rb +45 -0
  32. data/lib/dependabot/javascript/shared/metadata_finder.rb +209 -0
  33. data/lib/dependabot/javascript/shared/native_helpers.rb +21 -0
  34. data/lib/dependabot/javascript/shared/package_manager_detector.rb +72 -0
  35. data/lib/dependabot/javascript/shared/package_name.rb +118 -0
  36. data/lib/dependabot/javascript/shared/registry_helper.rb +190 -0
  37. data/lib/dependabot/javascript/shared/registry_parser.rb +93 -0
  38. data/lib/dependabot/javascript/shared/requirement.rb +144 -0
  39. data/lib/dependabot/javascript/shared/sub_dependency_files_filterer.rb +79 -0
  40. data/lib/dependabot/javascript/shared/update_checker/dependency_files_builder.rb +87 -0
  41. data/lib/dependabot/javascript/shared/update_checker/registry_finder.rb +358 -0
  42. data/lib/dependabot/javascript/shared/version.rb +133 -0
  43. data/lib/dependabot/javascript/shared/version_selector.rb +60 -0
  44. data/lib/dependabot/javascript.rb +39 -0
  45. metadata +327 -0
@@ -0,0 +1,359 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module Javascript
6
+ module Shared
7
+ module ConstraintHelper
8
+ extend T::Sig
9
+
10
+ # Regex Components for Semantic Versioning
11
+ DIGIT = "\\d+" # Matches a single number (e.g., "1")
12
+ PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
13
+ BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
14
+
15
+ # Matches semantic versions:
16
+ VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
17
+
18
+ VERSION_REGEX = T.let(/^#{VERSION}$/, Regexp)
19
+
20
+ # Base regex for SemVer (major.minor.patch[-prerelease][+build])
21
+ # This pattern extracts valid semantic versioning strings based on the SemVer 2.0 specification.
22
+ SEMVER_REGEX = T.let(/
23
+ (?<version>\d+\.\d+\.\d+) # Match major.minor.patch (e.g., 1.2.3)
24
+ (?:-(?<prerelease>[a-zA-Z0-9.-]+))? # Optional prerelease (e.g., -alpha.1, -rc.1, -beta.5)
25
+ (?:\+(?<build>[a-zA-Z0-9.-]+))? # Optional build metadata (e.g., +build.20231101, +exp.sha.5114f85)
26
+ /x, Regexp)
27
+
28
+ # Full SemVer validation regex (ensures the entire string is a valid SemVer)
29
+ # This ensures the entire input strictly follows SemVer, without extra characters before/after.
30
+ SEMVER_VALIDATION_REGEX = T.let(/^#{SEMVER_REGEX}$/, Regexp)
31
+
32
+ # SemVer constraint regex (supports package.json version constraints)
33
+ # This pattern ensures proper parsing of SemVer versions with optional operators.
34
+ SEMVER_CONSTRAINT_REGEX = T.let(/
35
+ (?: (>=|<=|>|<|=|~|\^)\s*)? # Make operators optional (e.g., >=, ^, ~)
36
+ (\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?) # Match full SemVer versions
37
+ | (\*|latest) # Match wildcard (*) or 'latest'
38
+ /x, Regexp)
39
+
40
+ # /(>=|<=|>|<|=|~|\^)\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)|(\*|latest)/
41
+
42
+ SEMVER_OPERATOR_REGEX = /^(>=|<=|>|<|~|\^|=)$/
43
+
44
+ # Constraint Types as Constants
45
+ CARET_CONSTRAINT_REGEX = T.let(/^\^\s*(#{VERSION})$/, Regexp)
46
+ TILDE_CONSTRAINT_REGEX = T.let(/^~\s*(#{VERSION})$/, Regexp)
47
+ EXACT_CONSTRAINT_REGEX = T.let(/^\s*(#{VERSION})$/, Regexp)
48
+ GREATER_THAN_EQUAL_REGEX = T.let(/^>=\s*(#{VERSION})$/, Regexp)
49
+ LESS_THAN_EQUAL_REGEX = T.let(/^<=\s*(#{VERSION})$/, Regexp)
50
+ GREATER_THAN_REGEX = T.let(/^>\s*(#{VERSION})$/, Regexp)
51
+ LESS_THAN_REGEX = T.let(/^<\s*(#{VERSION})$/, Regexp)
52
+ WILDCARD_REGEX = T.let(/^\*$/, Regexp)
53
+ LATEST_REGEX = T.let(/^latest$/, Regexp)
54
+ SEMVER_CONSTANTS = ["*", "latest"].freeze
55
+
56
+ # Unified Regex for Valid Constraints
57
+ VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
58
+ CARET_CONSTRAINT_REGEX,
59
+ TILDE_CONSTRAINT_REGEX,
60
+ EXACT_CONSTRAINT_REGEX,
61
+ GREATER_THAN_EQUAL_REGEX,
62
+ LESS_THAN_EQUAL_REGEX,
63
+ GREATER_THAN_REGEX,
64
+ LESS_THAN_REGEX,
65
+ WILDCARD_REGEX,
66
+ LATEST_REGEX
67
+ ).freeze, Regexp)
68
+
69
+ # Extract unique constraints from the given constraint expression.
70
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
71
+ # @return [T::Array[String]] The list of unique Ruby-compatible constraints.
72
+ sig do
73
+ params(
74
+ constraint_expression: T.nilable(String),
75
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
76
+ )
77
+ .returns(T.nilable(T::Array[String]))
78
+ end
79
+ def self.extract_ruby_constraints(constraint_expression, dependabot_versions = nil)
80
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
81
+
82
+ return nil unless parsed_constraints
83
+
84
+ parsed_constraints.filter_map { |parsed| parsed[:constraint] }
85
+ end
86
+
87
+ # rubocop:disable Metrics/AbcSize
88
+ # rubocop:disable Metrics/CyclomaticComplexity
89
+ # rubocop:disable Metrics/MethodLength
90
+ # rubocop:disable Metrics/PerceivedComplexity
91
+ sig do
92
+ params(constraint_expression: T.nilable(String))
93
+ .returns(T.nilable(T::Array[String]))
94
+ end
95
+ def self.split_constraints(constraint_expression)
96
+ normalized_constraint = constraint_expression&.strip
97
+ return [] if normalized_constraint.nil? || normalized_constraint.empty?
98
+
99
+ # Split constraints by logical OR (`||`)
100
+ constraint_groups = normalized_constraint.split("||")
101
+
102
+ # Split constraints by logical AND (`,`)
103
+ constraint_groups = constraint_groups.map do |or_constraint|
104
+ or_constraint.split(",").map(&:strip)
105
+ end.flatten
106
+
107
+ constraint_groups = constraint_groups.map do |constraint|
108
+ tokens = constraint.split(/\s+/).map(&:strip)
109
+
110
+ and_constraints = []
111
+
112
+ previous = T.let(nil, T.nilable(String))
113
+ operator = T.let(false, T.nilable(T::Boolean))
114
+ wildcard = T.let(false, T::Boolean)
115
+
116
+ tokens.each do |token|
117
+ token = token.strip
118
+ next if token.empty?
119
+
120
+ # Invalid constraint if wildcard and anything else
121
+ return nil if wildcard
122
+
123
+ # If token is one of the operators (>=, <=, >, <, ~, ^, =)
124
+ if token.match?(SEMVER_OPERATOR_REGEX)
125
+ wildcard = false
126
+ operator = true
127
+ # If token is wildcard or latest
128
+ elsif token.match?(/(\*|latest)/)
129
+ and_constraints << token
130
+ wildcard = true
131
+ operator = false
132
+ # If token is exact version (e.g., "1.2.3")
133
+ elsif token.match(VERSION_REGEX)
134
+ and_constraints << if operator
135
+ "#{previous}#{token}"
136
+ else
137
+ token
138
+ end
139
+ wildcard = false
140
+ operator = false
141
+ # If token is a valid constraint (e.g., ">=1.2.3", "<=2.0.0")
142
+ elsif token.match(VALID_CONSTRAINT_REGEX)
143
+ return nil if operator
144
+
145
+ and_constraints << token
146
+
147
+ wildcard = false
148
+ operator = false
149
+ else
150
+ # invalid constraint
151
+ return nil
152
+ end
153
+ previous = token
154
+ end
155
+ and_constraints.uniq
156
+ end.flatten
157
+ constraint_groups if constraint_groups.any?
158
+ end
159
+
160
+ # rubocop:enable Metrics/AbcSize
161
+ # rubocop:enable Metrics/CyclomaticComplexity
162
+ # rubocop:enable Metrics/MethodLength
163
+ # rubocop:enable Metrics/PerceivedComplexity
164
+
165
+ # Find the highest version from the given constraint expression.
166
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
167
+ # @return [T.nilable(String)] The highest version, or nil if no versions are available.
168
+ sig do
169
+ params(
170
+ constraint_expression: T.nilable(String),
171
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
172
+ )
173
+ .returns(T.nilable(String))
174
+ end
175
+ def self.find_highest_version_from_constraint_expression(constraint_expression, dependabot_versions = nil)
176
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
177
+
178
+ return nil unless parsed_constraints
179
+
180
+ parsed_constraints
181
+ .filter_map { |parsed| parsed[:version] } # Extract all versions
182
+ .max_by { |version| Version.new(version) }
183
+ end
184
+
185
+ # Parse all constraints (split by logical OR `||`) and convert to Ruby-compatible constraints.
186
+ # Return:
187
+ # - `nil` if the constraint expression is invalid
188
+ # - `[]` if the constraint expression is valid but represents "no constraints"
189
+ # - An array of hashes for valid constraints with details about the constraint and version
190
+ sig do
191
+ params(
192
+ constraint_expression: T.nilable(String),
193
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
194
+ )
195
+ .returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
196
+ end
197
+ def self.parse_constraints(constraint_expression, dependabot_versions = nil)
198
+ splitted_constraints = split_constraints(constraint_expression)
199
+
200
+ return unless splitted_constraints
201
+
202
+ constraints = to_ruby_constraints_with_versions(splitted_constraints, dependabot_versions)
203
+ constraints
204
+ end
205
+
206
+ sig do
207
+ params(
208
+ constraints: T::Array[String],
209
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
210
+ ).returns(T::Array[T::Hash[Symbol, T.nilable(String)]])
211
+ end
212
+ def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
213
+ constraints.filter_map do |constraint|
214
+ parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
215
+ parsed if parsed
216
+ end.uniq
217
+ end
218
+
219
+ # rubocop:disable Metrics/MethodLength
220
+ # rubocop:disable Metrics/PerceivedComplexity
221
+ # rubocop:disable Metrics/AbcSize
222
+ # rubocop:disable Metrics/CyclomaticComplexity
223
+ # Converts a semver constraint to a Ruby-compatible constraint and extracts the version, if available.
224
+ # @param constraint [String] The semver constraint to parse.
225
+ # @return [T.nilable(T::Hash[Symbol, T.nilable(String)])] Returns the Ruby-compatible
226
+ # constraint and the version, if available, or nil if the constraint is invalid.
227
+ #
228
+ # @example
229
+ # to_ruby_constraint_with_version("=1.2.3") # => { constraint: "=1.2.3", version: "1.2.3" }
230
+ # to_ruby_constraint_with_version("^1.2.3") # => { constraint: ">=1.2.3 <2.0.0", version: "1.2.3" }
231
+ # to_ruby_constraint_with_version("*") # => { constraint: nil, version: nil }
232
+ # to_ruby_constraint_with_version("invalid") # => nil
233
+ sig do
234
+ params(
235
+ constraint: String,
236
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
237
+ )
238
+ .returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
239
+ end
240
+ def self.to_ruby_constraint_with_version(constraint, dependabot_versions = [])
241
+ return nil if constraint.empty?
242
+
243
+ case constraint
244
+ when EXACT_CONSTRAINT_REGEX # Exact version, e.g., "1.2.3-alpha"
245
+ return unless Regexp.last_match
246
+
247
+ full_version = Regexp.last_match(1)
248
+ { constraint: "=#{full_version}", version: full_version }
249
+ when CARET_CONSTRAINT_REGEX # Caret constraint, e.g., "^1.2.3"
250
+ return unless Regexp.last_match
251
+
252
+ full_version = Regexp.last_match(1)
253
+ _, major, minor = version_components(full_version)
254
+ return nil if major.nil?
255
+
256
+ ruby_constraint =
257
+ if major.to_i.zero?
258
+ minor.nil? ? ">=#{full_version} <1.0.0" : ">=#{full_version} <0.#{minor.to_i + 1}.0"
259
+ else
260
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
261
+ end
262
+ { constraint: ruby_constraint, version: full_version }
263
+ when TILDE_CONSTRAINT_REGEX # Tilde constraint, e.g., "~1.2.3"
264
+ return unless Regexp.last_match
265
+
266
+ full_version = Regexp.last_match(1)
267
+ _, major, minor = version_components(full_version)
268
+ ruby_constraint =
269
+ if minor.nil?
270
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
271
+ else
272
+ ">=#{full_version} <#{major}.#{minor.to_i + 1}.0"
273
+ end
274
+ { constraint: ruby_constraint, version: full_version }
275
+ when GREATER_THAN_EQUAL_REGEX # Greater than or equal, e.g., ">=1.2.3"
276
+
277
+ return unless Regexp.last_match && Regexp.last_match(1)
278
+
279
+ found_version = highest_matching_version(
280
+ dependabot_versions,
281
+ T.must(Regexp.last_match(1))
282
+ ) do |version, constraint_version|
283
+ version >= Version.new(constraint_version)
284
+ end
285
+ { constraint: ">=#{Regexp.last_match(1)}", version: found_version&.to_s }
286
+ when LESS_THAN_EQUAL_REGEX # Less than or equal, e.g., "<=1.2.3"
287
+ return unless Regexp.last_match
288
+
289
+ full_version = Regexp.last_match(1)
290
+ { constraint: "<=#{full_version}", version: full_version }
291
+ when GREATER_THAN_REGEX # Greater than, e.g., ">1.2.3"
292
+ return unless Regexp.last_match && Regexp.last_match(1)
293
+
294
+ found_version = highest_matching_version(
295
+ dependabot_versions,
296
+ T.must(Regexp.last_match(1))
297
+ ) do |version, constraint_version|
298
+ version > Version.new(constraint_version)
299
+ end
300
+ { constraint: ">#{Regexp.last_match(1)}", version: found_version&.to_s }
301
+ when LESS_THAN_REGEX # Less than, e.g., "<1.2.3"
302
+ return unless Regexp.last_match && Regexp.last_match(1)
303
+
304
+ found_version = highest_matching_version(
305
+ dependabot_versions,
306
+ T.must(Regexp.last_match(1))
307
+ ) do |version, constraint_version|
308
+ version < Version.new(constraint_version)
309
+ end
310
+ { constraint: "<#{Regexp.last_match(1)}", version: found_version&.to_s }
311
+ when WILDCARD_REGEX # No specific constraint, resolves to the highest available version
312
+ { constraint: nil, version: dependabot_versions&.max&.to_s }
313
+ when LATEST_REGEX
314
+ { constraint: nil, version: dependabot_versions&.max&.to_s } # Resolves to the latest available version
315
+ end
316
+ end
317
+
318
+ sig do
319
+ params(
320
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version]),
321
+ constraint_version: String,
322
+ condition: T.proc.params(version: Dependabot::Version, constraint: Dependabot::Version).returns(T::Boolean)
323
+ )
324
+ .returns(T.nilable(Dependabot::Version))
325
+ end
326
+ def self.highest_matching_version(dependabot_versions, constraint_version, &condition)
327
+ return unless dependabot_versions&.any?
328
+
329
+ # Returns the highest version that satisfies the condition, or nil if none.
330
+ dependabot_versions
331
+ .sort
332
+ .reverse
333
+ .find { |version| condition.call(version, Version.new(constraint_version)) } # rubocop:disable Performance/RedundantBlockCall
334
+ end
335
+
336
+ # rubocop:enable Metrics/MethodLength
337
+ # rubocop:enable Metrics/PerceivedComplexity
338
+ # rubocop:enable Metrics/AbcSize
339
+ # rubocop:enable Metrics/CyclomaticComplexity
340
+
341
+ # Parses a semantic version string into its components as per the SemVer spec
342
+ # Example: "1.2.3-alpha+001" → ["1.2.3", "1", "2", "3", "alpha", "001"]
343
+ sig { params(full_version: T.nilable(String)).returns(T.nilable(T::Array[String])) }
344
+ def self.version_components(full_version)
345
+ return [] if full_version.nil?
346
+
347
+ match = full_version.match(SEMVER_VALIDATION_REGEX)
348
+ return [] unless match
349
+
350
+ version = match[:version]
351
+ return [] unless version
352
+
353
+ major, minor, patch = version.split(".")
354
+ [version, major, minor, patch, match[:prerelease], match[:build]].compact
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,164 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # lots of sub-projects that don't all have the same dependencies.
5
+ module Dependabot
6
+ module Javascript
7
+ module Shared
8
+ class DependencyFilesFilterer
9
+ extend T::Sig
10
+
11
+ sig do
12
+ params(
13
+ dependency_files: T::Array[DependencyFile],
14
+ updated_dependencies: T::Array[Dependency],
15
+ lockfile_parser_class: T.class_of(FileParser::LockfileParser)
16
+ )
17
+ .void
18
+ end
19
+ def initialize(dependency_files:, updated_dependencies:, lockfile_parser_class:)
20
+ @dependency_files = dependency_files
21
+ @updated_dependencies = updated_dependencies
22
+ @lockfile_parser_class = lockfile_parser_class
23
+ end
24
+
25
+ sig { returns(T::Array[String]) }
26
+ def paths_requiring_update_check
27
+ @paths_requiring_update_check ||= T.let(fetch_paths_requiring_update_check, T.nilable(T::Array[String]))
28
+ end
29
+
30
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
31
+ def files_requiring_update
32
+ @files_requiring_update ||= T.let(
33
+ dependency_files.select do |file|
34
+ package_files_requiring_update.include?(file) ||
35
+ package_required_lockfile?(file) ||
36
+ workspaces_lockfile?(file)
37
+ end, T.nilable(T::Array[DependencyFile])
38
+ )
39
+ end
40
+
41
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
42
+ def package_files_requiring_update
43
+ @package_files_requiring_update ||= T.let(
44
+ dependency_files.select do |file|
45
+ dependency_manifest_requirements.include?(file.name)
46
+ end, T.nilable(T::Array[DependencyFile])
47
+ )
48
+ end
49
+
50
+ private
51
+
52
+ sig { returns(T::Array[DependencyFile]) }
53
+ attr_reader :dependency_files
54
+
55
+ sig { returns(T::Array[Dependency]) }
56
+ attr_reader :updated_dependencies
57
+
58
+ sig { returns(T.class_of(FileParser::LockfileParser)) }
59
+ attr_reader :lockfile_parser_class
60
+
61
+ sig { returns(T::Array[String]) }
62
+ def fetch_paths_requiring_update_check
63
+ # if only a root lockfile exists, it tracks all dependencies
64
+ return [File.dirname(T.must(root_lockfile).name)] if lockfiles == [root_lockfile]
65
+
66
+ package_files_requiring_update.map do |file|
67
+ File.dirname(file.name)
68
+ end
69
+ end
70
+
71
+ sig { returns(T::Array[String]) }
72
+ def dependency_manifest_requirements
73
+ @dependency_manifest_requirements ||= T.let(
74
+ updated_dependencies.flat_map do |dep|
75
+ dep.requirements.map { |requirement| requirement[:file] }
76
+ end, T.nilable(T::Array[String])
77
+ )
78
+ end
79
+
80
+ sig { params(lockfile: DependencyFile).returns(T::Boolean) }
81
+ def package_required_lockfile?(lockfile)
82
+ return false unless lockfile?(lockfile)
83
+
84
+ package_files_requiring_update.any? do |package_file|
85
+ File.dirname(package_file.name) == File.dirname(lockfile.name)
86
+ end
87
+ end
88
+
89
+ sig { params(lockfile: DependencyFile).returns(T::Boolean) }
90
+ def workspaces_lockfile?(lockfile)
91
+ return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock"].include?(lockfile.name)
92
+
93
+ return false unless parsed_root_package_json["workspaces"] || dependency_files.any? do |file|
94
+ file.name.end_with?("pnpm-workspace.yaml") && File.dirname(file.name) == File.dirname(lockfile.name)
95
+ end
96
+
97
+ updated_dependencies_in_lockfile?(lockfile)
98
+ end
99
+
100
+ sig { returns(T.nilable(DependencyFile)) }
101
+ def root_lockfile
102
+ @root_lockfile ||= T.let(
103
+ lockfiles.find do |file|
104
+ File.dirname(file.name) == "."
105
+ end, T.nilable(DependencyFile)
106
+ )
107
+ end
108
+
109
+ sig { returns(T::Array[DependencyFile]) }
110
+ def lockfiles
111
+ @lockfiles ||= T.let(
112
+ dependency_files.select do |file|
113
+ lockfile?(file)
114
+ end, T.nilable(T::Array[DependencyFile])
115
+ )
116
+ end
117
+
118
+ sig { returns(T::Hash[String, T.untyped]) }
119
+ def parsed_root_package_json
120
+ @parsed_root_package_json ||= T.let(
121
+ begin
122
+ package = T.must(dependency_files.find { |f| f.name == "package.json" })
123
+ JSON.parse(T.must(package.content))
124
+ end, T.nilable(T::Hash[String, T.untyped])
125
+ )
126
+ end
127
+
128
+ sig { params(lockfile: Dependabot::DependencyFile).returns(T::Boolean) }
129
+ def updated_dependencies_in_lockfile?(lockfile)
130
+ lockfile_dependencies(lockfile).any? do |sub_dep|
131
+ updated_dependencies.any? do |updated_dep|
132
+ sub_dep.name == updated_dep.name
133
+ end
134
+ end
135
+ end
136
+
137
+ sig { params(lockfile: DependencyFile).returns(T::Array[Dependency]) }
138
+ def lockfile_dependencies(lockfile)
139
+ @lockfile_dependencies ||= T.let({}, T.nilable(T::Hash[String, T::Array[Dependency]]))
140
+ @lockfile_dependencies[lockfile.name] ||=
141
+ lockfile_parser_class.new(
142
+ dependency_files: [lockfile]
143
+ ).parse
144
+ end
145
+
146
+ sig { params(file: DependencyFile).returns(T::Boolean) }
147
+ def manifest?(file)
148
+ file.name.end_with?("package.json")
149
+ end
150
+
151
+ sig { params(file: DependencyFile).returns(T::Boolean) }
152
+ def lockfile?(file)
153
+ file.name.end_with?(
154
+ "package-lock.json",
155
+ "yarn.lock",
156
+ "pnpm-lock.yaml",
157
+ "bun.lock",
158
+ "npm-shrinkwrap.json"
159
+ )
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end