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,454 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # See https://docs.npmjs.com/files/package.json for package.json format docs.
5
+
6
+ module Dependabot
7
+ module Javascript
8
+ module Shared
9
+ class FileParser < Dependabot::FileParsers::Base
10
+ extend T::Sig
11
+
12
+ abstract!
13
+
14
+ DEPENDENCY_TYPES = T.let(%w(dependencies devDependencies optionalDependencies).freeze, T::Array[String])
15
+ GIT_URL_REGEX = %r{
16
+ (?<git_prefix>^|^git.*?|^github:|^bitbucket:|^gitlab:|github\.com/)
17
+ (?<username>[a-z0-9-]+)/
18
+ (?<repo>[a-z0-9_.-]+)
19
+ (
20
+ (?:\#semver:(?<semver>.+))|
21
+ (?:\#(?=[\^~=<>*])(?<semver>.+))|
22
+ (?:\#(?<ref>.+))
23
+ )?$
24
+ }ix
25
+ RC_FILENAME = T.let(".npmrc", String)
26
+
27
+ sig do
28
+ params(
29
+ json: T::Hash[String, T.untyped],
30
+ _block: T.proc.params(arg0: String, arg1: String, arg2: String).void
31
+ )
32
+ .void
33
+ end
34
+ def self.each_dependency(json, &_block)
35
+ DEPENDENCY_TYPES.each do |type|
36
+ deps = json[type] || {}
37
+ deps.each do |name, requirement|
38
+ yield(name, requirement, type)
39
+ end
40
+ end
41
+ end
42
+
43
+ sig { override.returns(T::Array[Dependency]) }
44
+ def parse # rubocop:disable Metrics/PerceivedComplexity
45
+ dependency_set = DependencySet.new
46
+ dependency_set += manifest_dependencies
47
+ dependency_set += lockfile_dependencies
48
+ dependency_set += workspace_catalog_dependencies if pnpm_workspace_yml
49
+
50
+ dependencies = self.class.dependencies_with_all_versions_metadata(dependency_set)
51
+
52
+ dependencies.reject do |dep|
53
+ reqs = dep.requirements
54
+
55
+ # Ignore dependencies defined in support files, since we don't want PRs for those
56
+ support_reqs = reqs.select { |r| support_package_files.any? { |f| f.name == r[:file] } }
57
+ next true if support_reqs.any?
58
+
59
+ # TODO: Currently, Dependabot can't handle dependencies that have both
60
+ # a git source *and* a non-git source. Fix that!
61
+ git_reqs = reqs.select { |r| r.dig(:source, :type) == "git" }
62
+ next false if git_reqs.none?
63
+ next true if git_reqs.map { |r| r.fetch(:source) }.uniq.count > 1
64
+
65
+ dep.requirements.any? { |r| r.dig(:source, :type) != "git" }
66
+ end
67
+ end
68
+
69
+ sig { abstract.returns(Ecosystem) }
70
+ def ecosystem; end
71
+
72
+ sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).returns(T::Array[Dependency]) }
73
+ def self.dependencies_with_all_versions_metadata(dependency_set)
74
+ dependency_set.dependencies.map do |dependency|
75
+ dependency.metadata[:all_versions] = dependency_set.all_versions_for_name(dependency.name)
76
+ dependency
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ sig { abstract.returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
83
+ def lockfiles; end
84
+
85
+ sig { abstract.returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
86
+ def registry_config_files; end
87
+
88
+ sig { returns(T.untyped) }
89
+ def parsed_package_json
90
+ JSON.parse(T.must(package_json.content))
91
+ rescue JSON::ParserError
92
+ raise Dependabot::DependencyFileNotParseable, package_json.path
93
+ end
94
+
95
+ sig { returns(Dependabot::DependencyFile) }
96
+ def package_json
97
+ # Declare the instance variable with T.let and the correct type
98
+ @package_json ||= T.let(
99
+ T.must(dependency_files.find { |f| f.name == MANIFEST_FILENAME }),
100
+ T.nilable(Dependabot::DependencyFile)
101
+ )
102
+ end
103
+
104
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
105
+ def npmrc
106
+ @npmrc ||= T.let(dependency_files.find do |f|
107
+ f.name.end_with?(RC_FILENAME)
108
+ end, T.nilable(Dependabot::DependencyFile))
109
+ end
110
+
111
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
112
+ def pnpm_workspace_yml
113
+ nil
114
+ end
115
+
116
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
117
+ def manifest_dependencies
118
+ dependency_set = DependencySet.new
119
+
120
+ package_files.each do |file|
121
+ json = JSON.parse(T.must(file.content))
122
+
123
+ # TODO: Currently, Dependabot can't handle flat dependency files
124
+ # (and will error at the FileUpdater stage, because the
125
+ # UpdateChecker doesn't take account of flat resolution).
126
+ next if json["flat"]
127
+
128
+ self.class.each_dependency(json) do |name, requirement, type|
129
+ next unless requirement.is_a?(String)
130
+
131
+ # Skip dependencies using Yarn workspace cross-references as requirements
132
+ next if requirement.start_with?("workspace:", "catalog:")
133
+
134
+ requirement = "*" if requirement == ""
135
+ dep = build_dependency(
136
+ file: file, type: type, name: name, requirement: requirement
137
+ )
138
+ dependency_set << dep if dep
139
+ end
140
+ end
141
+
142
+ dependency_set
143
+ end
144
+
145
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
146
+ def workspace_catalog_dependencies
147
+ dependency_set = DependencySet.new
148
+ workspace_config = YAML.safe_load(T.must(pnpm_workspace_yml&.content), aliases: true)
149
+
150
+ workspace_config["catalog"]&.each do |name, version|
151
+ dep = build_dependency(
152
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
153
+ )
154
+ dependency_set << dep if dep
155
+ end
156
+
157
+ workspace_config["catalogs"]&.each do |_, group_depenencies|
158
+ group_depenencies.each do |name, version|
159
+ dep = build_dependency(
160
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
161
+ )
162
+ dependency_set << dep if dep
163
+ end
164
+ end
165
+
166
+ dependency_set
167
+ end
168
+
169
+ sig { abstract.returns(FileParser::LockfileParser) }
170
+ def lockfile_parser; end
171
+
172
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
173
+ def lockfile_dependencies
174
+ lockfile_parser.parse_set
175
+ end
176
+
177
+ sig do
178
+ params(file: DependencyFile, type: T.untyped, name: String, requirement: String)
179
+ .returns(T.nilable(Dependency))
180
+ end
181
+ def build_dependency(file:, type:, name:, requirement:)
182
+ lockfile_details = lockfile_parser.lockfile_details(
183
+ dependency_name: name,
184
+ requirement: requirement,
185
+ manifest_name: file.name
186
+ )
187
+ version = version_for(requirement, lockfile_details)
188
+ converted_version = T.let(if version.nil?
189
+ nil
190
+ elsif version.is_a?(String)
191
+ version
192
+ else
193
+ Dependabot::Version.new(version)
194
+ end, T.nilable(T.any(String, Dependabot::Version)))
195
+
196
+ return if lockfile_details && !version
197
+ return if ignore_requirement?(requirement)
198
+ return if workspace_package_names.include?(name)
199
+
200
+ # TODO: Handle aliased packages:
201
+ # https://github.com/dependabot/dependabot-core/pull/1115
202
+ #
203
+ # Ignore dependencies with an alias in the name
204
+ # Example: "my-fetch-factory@npm:fetch-factory"
205
+ return if aliased_package_name?(name)
206
+
207
+ Dependency.new(
208
+ name: name,
209
+ version: converted_version,
210
+ package_manager: ecosystem.name,
211
+ requirements: [{
212
+ requirement: requirement_for(requirement),
213
+ file: file.name,
214
+ groups: [type],
215
+ source: source_for(name, requirement, lockfile_details)
216
+ }]
217
+ )
218
+ end
219
+
220
+ sig { override.void }
221
+ def check_required_files
222
+ return if get_original_file(MANIFEST_FILENAME)
223
+
224
+ raise DependencyFileNotFound.new(nil,
225
+ "#{MANIFEST_FILENAME} not found.")
226
+ end
227
+
228
+ sig { params(requirement: String).returns(T::Boolean) }
229
+ def ignore_requirement?(requirement)
230
+ return true if local_path?(requirement)
231
+ return true if non_git_url?(requirement)
232
+
233
+ # TODO: Handle aliased packages:
234
+ # https://github.com/dependabot/dependabot-core/pull/1115
235
+ alias_package?(requirement)
236
+ end
237
+
238
+ sig { params(requirement: String).returns(T::Boolean) }
239
+ def local_path?(requirement)
240
+ requirement.start_with?("link:", "file:", "/", "./", "../", "~/")
241
+ end
242
+
243
+ sig { params(requirement: String).returns(T::Boolean) }
244
+ def alias_package?(requirement)
245
+ requirement.start_with?("npm:")
246
+ end
247
+
248
+ sig { params(requirement: String).returns(T::Boolean) }
249
+ def non_git_url?(requirement)
250
+ requirement.include?("://") && !git_url?(requirement)
251
+ end
252
+
253
+ sig { params(requirement: String).returns(T::Boolean) }
254
+ def git_url?(requirement)
255
+ requirement.match?(GIT_URL_REGEX)
256
+ end
257
+
258
+ sig { params(requirement: String).returns(T::Boolean) }
259
+ def git_url_with_semver?(requirement)
260
+ return false unless git_url?(requirement)
261
+
262
+ !T.must(requirement.match(GIT_URL_REGEX)).named_captures.fetch("semver").nil?
263
+ end
264
+
265
+ sig { params(name: String).returns(T::Boolean) }
266
+ def aliased_package_name?(name)
267
+ name.include?("@npm:")
268
+ end
269
+
270
+ sig { returns(T::Array[String]) }
271
+ def workspace_package_names
272
+ @workspace_package_names ||= T.let(package_files.filter_map do |f|
273
+ JSON.parse(T.must(f.content))["name"]
274
+ end, T.nilable(T::Array[String]))
275
+ end
276
+
277
+ sig do
278
+ params(requirement: String, lockfile_details: T.nilable(T::Hash[String, T.untyped]))
279
+ .returns(T.nilable(T.any(String, Integer, Gem::Version)))
280
+ end
281
+ def version_for(requirement, lockfile_details)
282
+ if git_url_with_semver?(requirement)
283
+ semver_version = lockfile_version_for(lockfile_details)
284
+ return semver_version if semver_version
285
+
286
+ git_revision = git_revision_for(lockfile_details)
287
+ version_from_git_revision(requirement, git_revision) || git_revision
288
+ elsif git_url?(requirement)
289
+ git_revision_for(lockfile_details)
290
+ elsif lockfile_details
291
+ lockfile_version_for(lockfile_details)
292
+ else
293
+ exact_version = exact_version_for(requirement)
294
+ return unless exact_version
295
+
296
+ semver_version_for(exact_version)
297
+ end
298
+ end
299
+
300
+ sig { params(lockfile_details: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(String)) }
301
+ def git_revision_for(lockfile_details)
302
+ version = T.cast(lockfile_details&.fetch("version", nil), T.nilable(String))
303
+ resolved = T.cast(lockfile_details&.fetch("resolved", nil), T.nilable(String))
304
+ [
305
+ version&.split("#")&.last,
306
+ resolved&.split("#")&.last,
307
+ resolved&.split("/")&.last
308
+ ].find { |str| commit_sha?(str) }
309
+ end
310
+
311
+ sig { params(string: T.nilable(String)).returns(T::Boolean) }
312
+ def commit_sha?(string)
313
+ return false unless string.is_a?(String)
314
+
315
+ string.match?(/^[0-9a-f]{40}$/)
316
+ end
317
+
318
+ sig { params(requirement: String, git_revision: T.nilable(String)).returns(T.nilable(String)) }
319
+ def version_from_git_revision(requirement, git_revision)
320
+ tags =
321
+ Dependabot::GitMetadataFetcher.new(
322
+ url: git_source_for(requirement).fetch(:url),
323
+ credentials: credentials
324
+ ).tags
325
+ .select { |t| [t.commit_sha, t.tag_sha].include?(git_revision) }
326
+
327
+ tags.each do |t|
328
+ next unless t.name.match?(Dependabot::GitCommitChecker::VERSION_REGEX)
329
+
330
+ version = T.must(t.name.match(Dependabot::GitCommitChecker::VERSION_REGEX))
331
+ .named_captures.fetch("version")
332
+ next unless version_class.correct?(version)
333
+
334
+ return version
335
+ end
336
+
337
+ nil
338
+ rescue Dependabot::GitDependenciesNotReachable
339
+ nil
340
+ end
341
+
342
+ sig do
343
+ params(lockfile_details: T.nilable(T::Hash[String, T.untyped]))
344
+ .returns(T.nilable(T.any(String, Integer, Gem::Version)))
345
+ end
346
+ def lockfile_version_for(lockfile_details)
347
+ semver_version_for(lockfile_details&.fetch("version", ""))
348
+ end
349
+
350
+ sig { params(version: T.nilable(String)).returns(T.nilable(T.any(String, Integer, Gem::Version))) }
351
+ def semver_version_for(version)
352
+ version_class.semver_for(version)
353
+ end
354
+
355
+ sig { params(requirement: String).returns(T.nilable(String)) }
356
+ def exact_version_for(requirement)
357
+ req = requirement_class.new([requirement])
358
+ return unless req.exact?
359
+
360
+ req.requirements.first.last.to_s
361
+ rescue Gem::Requirement::BadRequirementError
362
+ # If it doesn't parse, it's definitely not exact
363
+ end
364
+
365
+ sig do
366
+ params(name: String, requirement: String, lockfile_details: T.nilable(T::Hash[String, T.untyped]))
367
+ .returns(T.nilable(T::Hash[Symbol, T.untyped]))
368
+ end
369
+ def source_for(name, requirement, lockfile_details)
370
+ return git_source_for(requirement) if git_url?(requirement)
371
+
372
+ resolved_url = lockfile_details&.fetch("resolved", nil)
373
+
374
+ resolution = lockfile_details&.fetch("resolution", nil)
375
+ package_match = resolution&.match(/__archiveUrl=(?<package_url>.+)/)
376
+ resolved_url = CGI.unescape(package_match.named_captures.fetch("package_url", "")) if package_match
377
+
378
+ return unless resolved_url
379
+ return unless resolved_url.start_with?("http")
380
+ return if resolved_url.match?(/(?<!pkg\.)github/)
381
+
382
+ RegistryParser.new(
383
+ resolved_url: resolved_url,
384
+ credentials: credentials
385
+ ).registry_source_for(name)
386
+ end
387
+
388
+ sig { params(requirement: String).returns(T.nilable(String)) }
389
+ def requirement_for(requirement)
390
+ return requirement unless git_url?(requirement)
391
+
392
+ details = T.must(requirement.match(GIT_URL_REGEX)).named_captures
393
+ details["semver"]
394
+ end
395
+
396
+ sig { params(requirement: String).returns(T::Hash[Symbol, T.untyped]) }
397
+ def git_source_for(requirement)
398
+ details = T.must(requirement.match(GIT_URL_REGEX)).named_captures
399
+ prefix = T.must(details.fetch("git_prefix"))
400
+
401
+ host = if prefix.include?("git@") || prefix.include?("://")
402
+ T.must(prefix.split("git@").last)
403
+ .sub(%r{.*?://}, "")
404
+ .sub(%r{[:/]$}, "")
405
+ .split("#").first
406
+ elsif prefix.include?("bitbucket") then "bitbucket.org"
407
+ elsif prefix.include?("gitlab") then "gitlab.com"
408
+ else
409
+ "github.com"
410
+ end
411
+
412
+ {
413
+ type: "git",
414
+ url: "https://#{host}/#{details['username']}/#{details['repo']}",
415
+ branch: nil,
416
+ ref: details["ref"] || "master"
417
+ }
418
+ end
419
+
420
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
421
+ def support_package_files
422
+ @support_package_files ||= T.let(sub_package_files.select(&:support_file?),
423
+ T.nilable(T::Array[DependencyFile]))
424
+ end
425
+
426
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
427
+ def sub_package_files
428
+ return T.must(@sub_package_files) if defined?(@sub_package_files)
429
+
430
+ files = dependency_files.select { |f| f.name.end_with?(MANIFEST_FILENAME) }
431
+ .reject { |f| f.name == MANIFEST_FILENAME }
432
+ .reject { |f| f.name.include?("node_modules/") }
433
+ @sub_package_files ||= T.let(files, T.nilable(T::Array[Dependabot::DependencyFile]))
434
+ end
435
+
436
+ sig { returns(T::Array[DependencyFile]) }
437
+ def package_files
438
+ @package_files ||= T.let(
439
+ [
440
+ dependency_files.find { |f| f.name == MANIFEST_FILENAME },
441
+ *sub_package_files
442
+ ].compact, T.nilable(T::Array[DependencyFile])
443
+ )
444
+ end
445
+
446
+ sig { abstract.returns(T.class_of(Version)) }
447
+ def version_class; end
448
+
449
+ sig { abstract.returns(T.class_of(Requirement)) }
450
+ def requirement_class; end
451
+ end
452
+ end
453
+ end
454
+ end