ecosystems-bibliothecary 14.2.0 → 15.0.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +9 -24
  4. data/bibliothecary.gemspec +5 -9
  5. data/lib/bibliothecary/analyser/analysis.rb +10 -5
  6. data/lib/bibliothecary/analyser/matchers.rb +7 -5
  7. data/lib/bibliothecary/analyser.rb +0 -30
  8. data/lib/bibliothecary/cli.rb +35 -26
  9. data/lib/bibliothecary/configuration.rb +1 -6
  10. data/lib/bibliothecary/dependency.rb +1 -4
  11. data/lib/bibliothecary/file_info.rb +7 -0
  12. data/lib/bibliothecary/parsers/bentoml.rb +0 -2
  13. data/lib/bibliothecary/parsers/bower.rb +0 -1
  14. data/lib/bibliothecary/parsers/cargo.rb +12 -10
  15. data/lib/bibliothecary/parsers/carthage.rb +51 -15
  16. data/lib/bibliothecary/parsers/clojars.rb +14 -18
  17. data/lib/bibliothecary/parsers/cocoapods.rb +100 -19
  18. data/lib/bibliothecary/parsers/cog.rb +0 -2
  19. data/lib/bibliothecary/parsers/conan.rb +156 -0
  20. data/lib/bibliothecary/parsers/conda.rb +0 -3
  21. data/lib/bibliothecary/parsers/cpan.rb +0 -2
  22. data/lib/bibliothecary/parsers/cran.rb +40 -19
  23. data/lib/bibliothecary/parsers/docker.rb +0 -2
  24. data/lib/bibliothecary/parsers/dub.rb +33 -8
  25. data/lib/bibliothecary/parsers/dvc.rb +0 -2
  26. data/lib/bibliothecary/parsers/elm.rb +13 -3
  27. data/lib/bibliothecary/parsers/go.rb +14 -5
  28. data/lib/bibliothecary/parsers/hackage.rb +132 -24
  29. data/lib/bibliothecary/parsers/haxelib.rb +14 -4
  30. data/lib/bibliothecary/parsers/hex.rb +37 -20
  31. data/lib/bibliothecary/parsers/homebrew.rb +0 -2
  32. data/lib/bibliothecary/parsers/julia.rb +0 -2
  33. data/lib/bibliothecary/parsers/maven.rb +35 -25
  34. data/lib/bibliothecary/parsers/meteor.rb +14 -4
  35. data/lib/bibliothecary/parsers/mlflow.rb +0 -2
  36. data/lib/bibliothecary/parsers/npm.rb +47 -59
  37. data/lib/bibliothecary/parsers/nuget.rb +23 -22
  38. data/lib/bibliothecary/parsers/ollama.rb +0 -2
  39. data/lib/bibliothecary/parsers/packagist.rb +0 -3
  40. data/lib/bibliothecary/parsers/pub.rb +0 -2
  41. data/lib/bibliothecary/parsers/pypi.rb +54 -35
  42. data/lib/bibliothecary/parsers/rubygems.rb +92 -27
  43. data/lib/bibliothecary/parsers/shard.rb +0 -1
  44. data/lib/bibliothecary/parsers/swift_pm.rb +77 -29
  45. data/lib/bibliothecary/parsers/vcpkg.rb +68 -17
  46. data/lib/bibliothecary/runner.rb +169 -22
  47. data/lib/bibliothecary/version.rb +1 -1
  48. data/lib/bibliothecary.rb +3 -10
  49. data/lib/dockerfile_parser.rb +1 -1
  50. data/lib/modelfile_parser.rb +8 -8
  51. metadata +2 -108
  52. data/.codeclimate.yml +0 -25
  53. data/.github/CONTRIBUTING.md +0 -195
  54. data/.github/workflows/ci.yml +0 -25
  55. data/.gitignore +0 -10
  56. data/.rspec +0 -2
  57. data/.rubocop.yml +0 -69
  58. data/.ruby-version +0 -1
  59. data/.tidelift +0 -1
  60. data/CODE_OF_CONDUCT.md +0 -74
  61. data/Gemfile +0 -34
  62. data/Rakefile +0 -18
  63. data/bin/console +0 -15
  64. data/bin/setup +0 -8
  65. data/lib/bibliothecary/multi_parsers/bundler_like_manifest.rb +0 -26
  66. data/lib/bibliothecary/multi_parsers/cyclonedx.rb +0 -170
  67. data/lib/bibliothecary/multi_parsers/dependencies_csv.rb +0 -155
  68. data/lib/bibliothecary/multi_parsers/json_runtime.rb +0 -22
  69. data/lib/bibliothecary/multi_parsers/spdx.rb +0 -149
  70. data/lib/bibliothecary/purl_util.rb +0 -37
  71. data/lib/bibliothecary/runner/multi_manifest_filter.rb +0 -92
  72. data/lib/sdl_parser.rb +0 -30
@@ -7,13 +7,12 @@ module Bibliothecary
7
7
  module Parsers
8
8
  class Nuget
9
9
  include Bibliothecary::Analyser
10
- extend Bibliothecary::MultiParsers::JSONRuntime
11
10
 
12
11
  def self.mapping
13
12
  {
14
13
  match_filename("Project.json") => {
15
14
  kind: "manifest",
16
- parser: :parse_json_runtime_manifest,
15
+ parser: :parse_project_json,
17
16
  },
18
17
  match_filename("Project.lock.json") => {
19
18
  kind: "lockfile",
@@ -46,9 +45,19 @@ module Bibliothecary
46
45
  }
47
46
  end
48
47
 
49
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
50
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
51
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
48
+ def self.parse_project_json(file_contents, options: {})
49
+ manifest = JSON.parse(file_contents)
50
+ dependencies = manifest.fetch("dependencies", {}).map do |name, requirement|
51
+ Dependency.new(
52
+ name: name,
53
+ requirement: requirement,
54
+ type: "runtime",
55
+ source: options.fetch(:filename, nil),
56
+ platform: platform_name
57
+ )
58
+ end
59
+ ParserResult.new(dependencies: dependencies)
60
+ end
52
61
 
53
62
  def self.parse_project_lock_json(file_contents, options: {})
54
63
  manifest = JSON.parse file_contents
@@ -68,32 +77,24 @@ module Bibliothecary
68
77
  def self.parse_packages_lock_json(file_contents, options: {})
69
78
  manifest = JSON.parse file_contents
70
79
 
71
- frameworks = {}
72
- manifest.fetch("dependencies", []).each do |framework, deps|
73
- frameworks[framework] = deps
74
- .reject { |_name, details| details["type"] == "Project" } # Projects do not have versions
80
+ # Merge dependencies from all target frameworks, deduping by name.
81
+ # Different frameworks may have different versions of the same package,
82
+ # but we can only report one version per package.
83
+ dependencies = manifest.fetch("dependencies", {}).flat_map do |_framework, deps|
84
+ deps
85
+ .reject { |_name, details| details["type"] == "Project" }
75
86
  .map do |name, details|
76
87
  Dependency.new(
77
88
  name: name,
78
- # 'resolved' has been set in all examples so far
79
- # so fallback to requested is pure paranoia
80
89
  requirement: details.fetch("resolved", details.fetch("requested", "*")),
81
90
  type: "runtime",
82
91
  source: options.fetch(:filename, nil),
83
92
  platform: platform_name
84
93
  )
85
94
  end
86
- end
95
+ end.uniq(&:name)
87
96
 
88
- unless frameworks.empty?
89
- # we should really return multiple manifests, but bibliothecary doesn't
90
- # do that yet so at least pick deterministically.
91
-
92
- # Note, frameworks can be empty, so remove empty ones and then return the last sorted item if any
93
- frameworks.delete_if { |_k, v| v.empty? }
94
- return ParserResult.new(dependencies: frameworks[frameworks.keys.max]) unless frameworks.empty?
95
- end
96
- ParserResult.new(dependencies: [])
97
+ ParserResult.new(dependencies: dependencies)
97
98
  end
98
99
 
99
100
  def self.parse_packages_config(file_contents, options: {})
@@ -201,7 +202,7 @@ module Bibliothecary
201
202
 
202
203
  def self.parse_paket_lock(file_contents, options: {})
203
204
  lines = file_contents.split("\n")
204
- package_version_re = /\s+(?<name>\S+)\s\((?<version>\d+\.\d+[.\d+[.\d+]*]*)\)/
205
+ package_version_re = /\s+(?<name>\S+)\s\((?<version>\d+(?:\.\d+)+)\)/
205
206
  packages = lines.select { |line| package_version_re.match(line) }.map { |line| package_version_re.match(line) }.map do |match|
206
207
  Dependency.new(
207
208
  name: match[:name].strip,
@@ -15,8 +15,6 @@ module Bibliothecary
15
15
  }
16
16
  end
17
17
 
18
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
19
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
20
18
 
21
19
  def self.parse_modelfile(file_contents, options: {})
22
20
  source = options.fetch(:filename, 'Modelfile')
@@ -20,9 +20,6 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
24
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
25
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
26
23
 
27
24
  def self.parse_lockfile(file_contents, options: {})
28
25
  manifest = JSON.parse file_contents
@@ -20,8 +20,6 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
24
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
25
23
 
26
24
  def self.parse_yaml_manifest(file_contents, options: {})
27
25
  manifest = YAML.load file_contents
@@ -88,34 +88,40 @@ module Bibliothecary
88
88
  }
89
89
  end
90
90
 
91
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
92
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
93
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
94
91
 
95
92
  def self.parser_pylock(file_contents, options: {})
96
- lockfile = Tomlrb.parse(file_contents)
97
- dependencies = lockfile["packages"].map do |d|
98
- is_local = true if d.key?("archive") || d.key?("directory")
99
- Dependency.new(
93
+ dependencies = []
94
+ # Split into [[packages]] blocks and extract fields from each
95
+ file_contents.split(/\[\[packages\]\]/).drop(1).each do |block|
96
+ name = block[/^name\s*=\s*"([^"]+)"/m, 1]
97
+ version = block[/^version\s*=\s*"([^"]+)"/m, 1]
98
+ # Local packages have [packages.archive] or [packages.directory] sections
99
+ is_local = block.include?("[packages.archive]") || block.include?("[packages.directory]")
100
+
101
+ dependencies << Dependency.new(
100
102
  platform: platform_name,
101
- name: d["name"],
103
+ name: name,
102
104
  type: "runtime",
103
105
  source: options.fetch(:filename, nil),
104
- requirement: d["version"] || "*",
105
- local: is_local
106
+ requirement: version || "*",
107
+ local: is_local || nil
106
108
  )
107
109
  end
108
110
  ParserResult.new(dependencies: dependencies)
109
111
  end
110
112
 
111
113
  def self.parse_uv_lock(file_contents, options: {})
112
- manifest = Tomlrb.parse(file_contents)
113
114
  source = options.fetch(:filename, nil)
114
- dependencies = manifest.fetch("package", []).map do |package|
115
- Dependency.new(
115
+ dependencies = []
116
+ # Split into [[package]] blocks and extract fields from each
117
+ file_contents.split(/\[\[package\]\]/).drop(1).each do |block|
118
+ name = block[/name\s*=\s*"([^"]+)"/, 1]
119
+ version = block[/version\s*=\s*"([^"]+)"/, 1]
120
+
121
+ dependencies << Dependency.new(
116
122
  platform: platform_name,
117
- name: package["name"],
118
- requirement: map_requirements(package),
123
+ name: name,
124
+ requirement: version,
119
125
  type: "runtime", # All dependencies are considered runtime
120
126
  source: source
121
127
  )
@@ -230,32 +236,43 @@ module Bibliothecary
230
236
  end
231
237
 
232
238
  def self.parse_poetry_lock(file_contents, options: {})
233
- manifest = Tomlrb.parse(file_contents)
234
239
  deps = []
235
- manifest["package"].each do |package|
236
- # next if group == "_meta"
240
+ # Split into [[package]] blocks and extract fields from each
241
+ file_contents.split(/\[\[package\]\]/).drop(1).each do |block|
242
+ name = block[/^name\s*=\s*"([^"]+)"/m, 1]
243
+ version = block[/^version\s*=\s*"([^"]+)"/m, 1]
237
244
 
238
245
  # Poetry <1.2.0 used singular "category" for kind
239
246
  # Poetry >=1.2.0 uses plural "groups" field for kind(s)
240
- groups = package.values_at("category", "groups").flatten.compact
241
- .map do |g|
242
- if g == "dev"
243
- "develop"
244
- else
245
- (g == "main" ? "runtime" : g)
246
- end
247
+ # Use ^ anchor to avoid matching commented lines
248
+ category = block[/^category\s*=\s*"([^"]+)"/m, 1]
249
+ groups_match = block[/^groups\s*=\s*\[([^\]]+)\]/m, 1]
250
+ groups = if groups_match
251
+ groups_match.scan(/"([^"]+)"/).flatten
252
+ elsif category
253
+ [category]
254
+ else
255
+ []
256
+ end
257
+
258
+ groups = groups.map do |g|
259
+ if g == "dev"
260
+ "develop"
261
+ else
262
+ (g == "main" ? "runtime" : g)
247
263
  end
264
+ end
248
265
 
249
266
  groups = ["runtime"] if groups.empty?
250
267
 
251
268
  groups.each do |group|
252
- # Poetry lockfiles should already contain normalizated names, but we'll
269
+ # Poetry lockfiles should already contain normalized names, but we'll
253
270
  # apply it here as well just to be consistent with pyproject.toml parsing.
254
- normalized_name = normalize_name(package["name"])
271
+ normalized_name = normalize_name(name)
255
272
  deps << Dependency.new(
256
273
  name: normalized_name,
257
- original_name: normalized_name == package["name"] ? nil : package["name"],
258
- requirement: map_requirements(package),
274
+ original_name: normalized_name == name ? nil : name,
275
+ requirement: version,
259
276
  type: group,
260
277
  source: options.fetch(:filename, nil),
261
278
  platform: platform_name
@@ -314,7 +331,8 @@ module Bibliothecary
314
331
  # Invalid lines in requirements.txt are skipped.
315
332
  def self.parse_requirements_txt(file_contents, options: {})
316
333
  deps = []
317
- type = case options[:filename]
334
+ source = options.fetch(:filename, nil)
335
+ type = case source
318
336
  when /dev/ || /docs/ || /tools/
319
337
  "development"
320
338
  when /test/
@@ -323,21 +341,22 @@ module Bibliothecary
323
341
  "runtime"
324
342
  end
325
343
 
326
- file_contents.split("\n").each do |line|
327
- if line["://"]
344
+ file_contents.each_line do |line|
345
+ line = line.chomp
346
+ if line.include?("://")
328
347
  begin
329
- result = parse_requirements_txt_url(line, type, options.fetch(:filename, nil))
348
+ result = parse_requirements_txt_url(line, type, source)
330
349
  rescue URI::Error, NoEggSpecified
331
350
  next
332
351
  end
333
352
 
334
353
  deps << result
335
- elsif (match = line.delete(" ").match(REQUIREMENTS_REGEXP))
354
+ elsif (match = line.tr(" ", "").match(REQUIREMENTS_REGEXP))
336
355
  deps << Dependency.new(
337
356
  name: match[1],
338
357
  requirement: match[-1],
339
358
  type: type,
340
- source: options.fetch(:filename, nil),
359
+ source: source,
341
360
  platform: platform_name
342
361
  )
343
362
  end
@@ -1,18 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler"
4
- require "gemnasium/parser"
5
4
 
6
5
  module Bibliothecary
7
6
  module Parsers
8
7
  class Rubygems
9
8
  include Bibliothecary::Analyser
10
- extend Bibliothecary::MultiParsers::BundlerLikeManifest
11
9
 
12
10
  NAME_VERSION = '(?! )(.*?)(?: \(([^-]*)(?:-(.*))?\))?'
13
11
  NAME_VERSION_4 = /^ {4}#{NAME_VERSION}$/
14
12
  BUNDLED_WITH = /BUNDLED WITH/
15
13
 
14
+ # Gemfile patterns
15
+ GEM_REGEXP = /^\s*gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?/
16
+ GROUP_START = /^\s*group\s+(.+?)\s+do/
17
+ BLOCK_END = /^\s*end\s*$/
18
+
19
+ # Gemspec pattern - captures type in first group
20
+ GEMSPEC_DEPENDENCY = /\.add_(development_|runtime_)?dependency\s*\(?\s*['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?(?:\s*,\s*['"]([^'"]+)['"])?\s*\)?/
21
+
16
22
  def self.mapping
17
23
  {
18
24
  match_filenames("Gemfile", "gems.rb") => {
@@ -33,55 +39,114 @@ module Bibliothecary
33
39
  }
34
40
  end
35
41
 
36
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
37
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
38
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
39
42
 
40
43
  def self.parse_gemfile_lock(file_contents, options: {})
41
- lockfile = Bundler::LockfileParser.new(file_contents)
42
44
  source = options.fetch(:filename, nil)
45
+ dependencies = []
43
46
 
44
- dependencies = lockfile.specs.map do |spec|
45
- Dependency.new(
46
- platform: platform_name,
47
- name: spec.name,
48
- requirement: spec.version.to_s,
49
- type: "runtime",
50
- source: source
51
- )
52
- end
47
+ file_contents.each_line do |line|
48
+ line = line.chomp.gsub(/\r$/, "")
49
+ next unless (match = line.match(NAME_VERSION_4))
50
+
51
+ name, version, _platform = match.captures
52
+ next if name.nil? || name.empty?
53
53
 
54
- bundler_version = lockfile.bundler_version
55
- if bundler_version
56
54
  dependencies << Dependency.new(
57
55
  platform: platform_name,
58
- name: "bundler",
59
- requirement: bundler_version.to_s,
56
+ name: name,
57
+ requirement: version,
60
58
  type: "runtime",
61
59
  source: source
62
60
  )
63
61
  end
64
62
 
63
+ if (bundler_dep = parse_bundler(file_contents, source))
64
+ dependencies << bundler_dep
65
+ end
66
+
65
67
  ParserResult.new(dependencies: dependencies)
66
68
  end
67
69
 
68
70
  def self.parse_gemfile(file_contents, options: {})
69
- manifest = Gemnasium::Parser.send(:gemfile, file_contents)
70
- dependencies = parse_ruby_manifest(manifest, platform_name, options.fetch(:filename, nil))
71
- ParserResult.new(dependencies: dependencies)
71
+ source = options.fetch(:filename, nil)
72
+ deps = []
73
+ current_type = "runtime"
74
+ block_depth = 0
75
+
76
+ file_contents.each_line do |line|
77
+ # Track group blocks
78
+ if (group_match = line.match(GROUP_START))
79
+ block_depth += 1
80
+ groups = group_match[1]
81
+ current_type = groups.include?(":development") ? "development" : "runtime"
82
+ next
83
+ end
84
+
85
+ if line.match?(BLOCK_END) && block_depth > 0
86
+ block_depth -= 1
87
+ current_type = "runtime" if block_depth == 0
88
+ next
89
+ end
90
+
91
+ # Match gem declarations
92
+ if (match = line.match(GEM_REGEXP))
93
+ name = match[1]
94
+ version = match[2]
95
+ requirement = version ? "= #{version}" : ">= 0"
96
+
97
+ deps << Dependency.new(
98
+ platform: platform_name,
99
+ name: name,
100
+ requirement: requirement,
101
+ type: current_type,
102
+ source: source
103
+ )
104
+ end
105
+ end
106
+
107
+ ParserResult.new(dependencies: deps)
72
108
  end
73
109
 
74
110
  def self.parse_gemspec(file_contents, options: {})
75
- manifest = Gemnasium::Parser.send(:gemspec, file_contents)
76
- dependencies = parse_ruby_manifest(manifest, platform_name, options.fetch(:filename, nil))
77
- ParserResult.new(dependencies: dependencies)
111
+ source = options.fetch(:filename, nil)
112
+ deps = []
113
+
114
+ file_contents.each_line do |line|
115
+ match = line.match(GEMSPEC_DEPENDENCY)
116
+ next unless match
117
+
118
+ type_prefix, name, ver1, ver2 = match.captures
119
+ type = type_prefix == "development_" ? "development" : "runtime"
120
+ requirement = build_requirement(ver1, ver2)
121
+
122
+ deps << Dependency.new(
123
+ platform: platform_name,
124
+ name: name,
125
+ requirement: requirement,
126
+ type: type,
127
+ source: source
128
+ )
129
+ end
130
+
131
+ ParserResult.new(dependencies: deps)
132
+ end
133
+
134
+ def self.build_requirement(ver1, ver2)
135
+ if ver1 && ver2
136
+ "#{ver1}, #{ver2}"
137
+ elsif ver1
138
+ ver1
139
+ else
140
+ ">= 0"
141
+ end
78
142
  end
79
143
 
80
144
  def self.parse_bundler(file_contents, source = nil)
81
145
  bundled_with_index = file_contents.lines(chomp: true).find_index { |line| line.match(BUNDLED_WITH) }
82
- version = file_contents.lines(chomp: true).fetch(bundled_with_index + 1)&.strip
146
+ return nil unless bundled_with_index
83
147
 
84
- return nil unless version
148
+ version = file_contents.lines(chomp: true).fetch(bundled_with_index + 1, nil)&.strip
149
+ return nil unless version && !version.empty?
85
150
 
86
151
  Dependency.new(
87
152
  name: "bundler",
@@ -20,7 +20,6 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
24
23
 
25
24
  def self.parse_yaml_lockfile(file_contents, options: {})
26
25
  manifest = YAML.load file_contents
@@ -1,75 +1,123 @@
1
+ require "json"
2
+
1
3
  module Bibliothecary
2
4
  module Parsers
3
5
  class SwiftPM
4
6
  include Bibliothecary::Analyser
5
7
 
8
+ # Matches .Package(url: "...", majorVersion: X, minor: Y)
9
+ # Also matches .package(url: "...", from: "X.Y.Z") (Swift 4+)
10
+ PACKAGE_REGEXP_LEGACY = /\.Package\s*\(\s*url:\s*"([^"]+)"[^)]*majorVersion:\s*(\d+)[^)]*minor:\s*(\d+)/
11
+ PACKAGE_REGEXP_FROM = /\.package\s*\(\s*(?:name:\s*"[^"]+",\s*)?url:\s*"([^"]+)"[^)]*from:\s*"([^"]+)"/i
12
+ PACKAGE_REGEXP_EXACT = /\.package\s*\(\s*(?:name:\s*"[^"]+",\s*)?url:\s*"([^"]+)"[^)]*(?:\.exact|exact)\s*\(\s*"([^"]+)"\s*\)/i
13
+ PACKAGE_REGEXP_RANGE = /\.package\s*\(\s*(?:name:\s*"[^"]+",\s*)?url:\s*"([^"]+)"[^)]*"([^"]+)"\s*(?:\.\.|\.\.\.)\s*"([^"]+)"/i
14
+
6
15
  def self.mapping
7
16
  {
8
17
  match_filename("Package.swift", case_insensitive: true) => {
9
- kind: 'manifest',
18
+ kind: "manifest",
10
19
  parser: :parse_package_swift,
11
- related_to: ['lockfile']
20
+ related_to: ["lockfile"],
12
21
  },
13
22
  match_filename("Package.resolved", case_insensitive: true) => {
14
- kind: 'lockfile',
23
+ kind: "lockfile",
15
24
  parser: :parse_package_resolved,
16
- related_to: ['manifest']
17
- }
25
+ related_to: ["manifest"],
26
+ },
18
27
  }
19
28
  end
20
29
 
21
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
24
30
 
25
31
  def self.parse_package_swift(file_contents, options: {})
26
- source = options.fetch(:filename, 'Package.swift')
27
- response = Typhoeus.post("#{Bibliothecary.configuration.swift_parser_host}/to-json", body: file_contents, timeout: 60)
28
- raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.swift_parser_host}/to-json", response.response_code) unless response.success?
29
- json = JSON.parse(response.body)
30
- deps = json["dependencies"].map do |dependency|
31
- name = dependency["url"].gsub(/^https?:\/\//, "").gsub(/\.git$/,"")
32
- version = "#{dependency['version']['lowerBound']} - #{dependency['version']['upperBound']}"
33
- Bibliothecary::Dependency.new(
32
+ source = options.fetch(:filename, "Package.swift")
33
+ deps = []
34
+
35
+ # Remove comments (but not :// in URLs)
36
+ content = file_contents.gsub(%r{(?<!:)//.*$}, "")
37
+
38
+ # Legacy format: .Package(url: "...", majorVersion: X, minor: Y)
39
+ content.scan(PACKAGE_REGEXP_LEGACY) do |url, major, minor|
40
+ name = url.gsub(%r{^https?://}, "").gsub(/\.git$/, "")
41
+ # Match the remote parser format: lowerBound - upperBound
42
+ lower = "#{major}.#{minor}.0"
43
+ upper = "#{major}.#{minor}.9223372036854775807"
44
+ deps << Dependency.new(
45
+ platform: platform_name,
46
+ name: name,
47
+ requirement: "#{lower} - #{upper}",
48
+ type: "runtime",
49
+ source: source
50
+ )
51
+ end
52
+
53
+ # Swift 4+ format: .package(url: "...", from: "X.Y.Z")
54
+ content.scan(PACKAGE_REGEXP_FROM) do |url, version|
55
+ name = url.gsub(%r{^https?://}, "").gsub(/\.git$/, "")
56
+ deps << Dependency.new(
34
57
  platform: platform_name,
35
58
  name: name,
36
- requirement: version,
59
+ requirement: ">= #{version}",
37
60
  type: "runtime",
38
61
  source: source
39
62
  )
40
63
  end
41
- Bibliothecary::ParserResult.new(dependencies: deps)
64
+
65
+ # Swift 4+ exact version: .package(url: "...", .exact("X.Y.Z"))
66
+ content.scan(PACKAGE_REGEXP_EXACT) do |url, version|
67
+ name = url.gsub(%r{^https?://}, "").gsub(/\.git$/, "")
68
+ deps << Dependency.new(
69
+ platform: platform_name,
70
+ name: name,
71
+ requirement: "= #{version}",
72
+ type: "runtime",
73
+ source: source
74
+ )
75
+ end
76
+
77
+ # Swift 4+ range: .package(url: "...", "1.0.0"..<"2.0.0")
78
+ content.scan(PACKAGE_REGEXP_RANGE) do |url, lower, upper|
79
+ name = url.gsub(%r{^https?://}, "").gsub(/\.git$/, "")
80
+ deps << Dependency.new(
81
+ platform: platform_name,
82
+ name: name,
83
+ requirement: "#{lower} - #{upper}",
84
+ type: "runtime",
85
+ source: source
86
+ )
87
+ end
88
+
89
+ ParserResult.new(dependencies: deps)
42
90
  end
43
91
 
44
92
  def self.parse_package_resolved(file_contents, options: {})
45
- source = options.fetch(:filename, 'Package.resolved')
93
+ source = options.fetch(:filename, "Package.resolved")
46
94
  json = JSON.parse(file_contents)
47
95
  deps = if json["version"] == 1
48
96
  json["object"]["pins"].map do |dependency|
49
- name = dependency['repositoryURL'].gsub(/^https?:\/\//, '').gsub(/\.git$/,'')
50
- version = dependency['state']['version']
51
- Bibliothecary::Dependency.new(
97
+ name = dependency["repositoryURL"].gsub(%r{^https?://}, "").gsub(/\.git$/, "")
98
+ version = dependency["state"]["version"]
99
+ Dependency.new(
52
100
  platform: platform_name,
53
101
  name: name,
54
102
  requirement: version,
55
- type: 'runtime',
103
+ type: "runtime",
56
104
  source: source
57
105
  )
58
106
  end
59
- else # version 2
107
+ else # version 2+
60
108
  json["pins"].map do |dependency|
61
- name = dependency['location'].gsub(/^https?:\/\//, '').gsub(/\.git$/,'')
62
- version = dependency['state']['version']
63
- Bibliothecary::Dependency.new(
109
+ name = dependency["location"].gsub(%r{^https?://}, "").gsub(/\.git$/, "")
110
+ version = dependency["state"]["version"]
111
+ Dependency.new(
64
112
  platform: platform_name,
65
113
  name: name,
66
114
  requirement: version,
67
- type: 'runtime',
115
+ type: "runtime",
68
116
  source: source
69
117
  )
70
118
  end
71
119
  end
72
- Bibliothecary::ParserResult.new(dependencies: deps)
120
+ ParserResult.new(dependencies: deps)
73
121
  end
74
122
  end
75
123
  end