bibliothecary 8.2.0 → 8.2.3

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bibliothecary/multi_parsers/dependencies_csv.rb +151 -0
  3. data/lib/bibliothecary/parsers/bower.rb +2 -0
  4. data/lib/bibliothecary/parsers/cargo.rb +1 -0
  5. data/lib/bibliothecary/parsers/carthage.rb +2 -0
  6. data/lib/bibliothecary/parsers/clojars.rb +2 -0
  7. data/lib/bibliothecary/parsers/cocoapods.rb +2 -0
  8. data/lib/bibliothecary/parsers/conda.rb +1 -0
  9. data/lib/bibliothecary/parsers/cpan.rb +2 -0
  10. data/lib/bibliothecary/parsers/cran.rb +1 -0
  11. data/lib/bibliothecary/parsers/dub.rb +2 -0
  12. data/lib/bibliothecary/parsers/elm.rb +2 -0
  13. data/lib/bibliothecary/parsers/go.rb +1 -0
  14. data/lib/bibliothecary/parsers/hackage.rb +1 -0
  15. data/lib/bibliothecary/parsers/haxelib.rb +3 -0
  16. data/lib/bibliothecary/parsers/hex.rb +1 -0
  17. data/lib/bibliothecary/parsers/julia.rb +2 -0
  18. data/lib/bibliothecary/parsers/maven.rb +12 -11
  19. data/lib/bibliothecary/parsers/meteor.rb +2 -0
  20. data/lib/bibliothecary/parsers/npm.rb +1 -0
  21. data/lib/bibliothecary/parsers/nuget.rb +1 -0
  22. data/lib/bibliothecary/parsers/packagist.rb +1 -0
  23. data/lib/bibliothecary/parsers/pub.rb +2 -0
  24. data/lib/bibliothecary/parsers/pypi.rb +1 -0
  25. data/lib/bibliothecary/parsers/rubygems.rb +1 -0
  26. data/lib/bibliothecary/parsers/shard.rb +2 -0
  27. data/lib/bibliothecary/parsers/swift_pm.rb +1 -0
  28. data/lib/bibliothecary/related_files_info.rb +32 -8
  29. data/lib/bibliothecary/runner/multi_manifest_filter.rb +67 -0
  30. data/lib/bibliothecary/runner.rb +37 -12
  31. data/lib/bibliothecary/version.rb +1 -1
  32. data/lib/bibliothecary.rb +2 -0
  33. metadata +4 -3
  34. data/lib/bibliothecary/parsers/generic.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dfafc16f9be53462f1eca5e0fa5b09dcc0a5d92531570630c087773ff87d18c
4
- data.tar.gz: 3d0045a95cc5cff513474aae337e0a032543130cc7b87144adad8033138c6e17
3
+ metadata.gz: 6bf3193b6daf685565ca3fe2105ca8be8e03c5e83d1edcafe3ca6d3c7be12d8b
4
+ data.tar.gz: c2b96281c11a89bf49f830bbb0be00e35d18096dcc8a8e8b2d0038e1256a6eed
5
5
  SHA512:
6
- metadata.gz: f984d7445345768463af5f2cb0906bf997e56ca03842ca538b65d95764ac427ed448a062593e293eeaa33779aaa2cb9b59b18f0715de813a6f018a815a03dc3f
7
- data.tar.gz: 067f0a20d820635cd697b18c43254a818284a66b98a13367146a0f9063b63bfe9316427d32bf6b88d960f6283dd56adb4ad2f874c003c7e4c151d71181f3cd37
6
+ metadata.gz: d3d45d37ac4c8e3c982d708f454906ab41db86fc285ba800d90acb6d2e541ae7151e7d09c5ae10eccec2678760b24f2bbb3d5c2e77048b714aa71c953395ce70
7
+ data.tar.gz: b86748a552b1774895cad46925205a7c4915907d336436c945a7dc027d80e8feff7d12465c9524ea12a3495d4635e45c4cd32489456453b4c744a22fdf7c5c32
@@ -0,0 +1,151 @@
1
+ require 'csv'
2
+
3
+ module Bibliothecary
4
+ module MultiParsers
5
+ module DependenciesCSV
6
+ include Bibliothecary::Analyser
7
+ include Bibliothecary::Analyser::TryCache
8
+
9
+ def self.mapping
10
+ {
11
+ match_filename('dependencies.csv') => {
12
+ kind: 'lockfile',
13
+ parser: :parse_dependencies_csv
14
+ }
15
+ }
16
+ end
17
+
18
+ # Processing a CSV file isn't as exact as using a real manifest file,
19
+ # but you can get pretty close as long as the data you're importing
20
+ # is simple.
21
+ class CSVFile
22
+ # Header structures are:
23
+ #
24
+ # <field to fill in for dependency> => {
25
+ # match: [<regexp of incoming column name to match in priority order, highest priority first>...],
26
+ # [default]: <optional default value for this field>
27
+ # }
28
+ HEADERS = {
29
+ "platform" => {
30
+ match: [
31
+ /^platform$/i
32
+ ]
33
+ },
34
+ "name" => {
35
+ match: [
36
+ /^name$/i
37
+ ]
38
+ },
39
+ # Lockfiles have exact versions.
40
+ "lockfile_requirement" => {
41
+ match: [
42
+ /^(lockfile |)requirement$/i,
43
+ /^version$/i,
44
+ ],
45
+ },
46
+ # Manifests have versions that can have operators.
47
+ # However, since Bibliothecary only currently supports analyzing a
48
+ # single file as a single thing (either manifest or lockfile)
49
+ # we can't return manifest-y data. Only take the lockfile requirement
50
+ # when processing dependencies.csv for now.
51
+ "requirement" => {
52
+ match: [
53
+ /^(lockfile |)requirement$/i,
54
+ /^version$/i,
55
+ ],
56
+ },
57
+ "type" => {
58
+ default: "runtime",
59
+ match: [
60
+ /^(lockfile |)type$/i,
61
+ /^(manifest |)type$/i
62
+ ]
63
+ }
64
+ }
65
+
66
+ attr_reader :result
67
+
68
+ def initialize(file_contents)
69
+ @file_contents = file_contents
70
+
71
+ @result = nil
72
+
73
+ # A Hash of "our field name" => ["header in CSV file", "lower priority header in CSV file"]
74
+ @header_mappings = {}
75
+ end
76
+
77
+ def parse!
78
+ table = parse_and_validate_csv_file
79
+
80
+ @result = table.map.with_index do |row, idx|
81
+ HEADERS.each_with_object({}) do |(header, info), obj|
82
+ # find the first non-empty field in the row for this header, or nil if not found
83
+ row_data = row[@header_mappings[header]]
84
+
85
+ # some column have default data to fall back on
86
+ if row_data
87
+ obj[header.to_sym] = row_data
88
+ elsif info.has_key?(:default)
89
+ # if the default is nil, don't even add the key to the hash
90
+ obj[header.to_sym] = info[:default] if info[:default]
91
+ else
92
+ # use 1-based index just like the 'csv' std lib, and count the headers as first row.
93
+ raise "Missing required field '#{header}' on line #{idx + 2}."
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def parse_and_validate_csv_file
102
+ table = CSV.parse(@file_contents, headers: true)
103
+
104
+ header_examination_results = map_table_headers_to_local_lookups(table, HEADERS)
105
+ unless header_examination_results[:missing].empty?
106
+ raise "Missing required headers #{header_examination_results[:missing].join(', ')} in CSV. Check to make sure header names are all lowercase."
107
+ end
108
+ @header_mappings = header_examination_results[:found]
109
+
110
+ table
111
+ end
112
+
113
+ def map_table_headers_to_local_lookups(table, local_lookups)
114
+ result = local_lookups.each_with_object({ found: {}, missing: [] }) do |(header, info), obj|
115
+ results = table.headers.each_with_object([]) do |table_header, matches|
116
+ info[:match].each_with_index do |match_regexp, index|
117
+ matches << [table_header, index] if table_header[match_regexp]
118
+ end
119
+ end
120
+
121
+ if results.empty?
122
+ # if a header has a default value it's optional
123
+ obj[:missing] << header unless info.has_key?(:default)
124
+ else
125
+ # select the highest priority header possible
126
+ obj[:found][header] ||= nil
127
+ obj[:found][header] = ([obj[:found][header]] + results).compact.min_by(&:last)
128
+ end
129
+ end
130
+
131
+ # strip off the priorities. only one mapping should remain.
132
+ result[:found].transform_values!(&:first)
133
+
134
+ result
135
+ end
136
+ end
137
+
138
+ def parse_dependencies_csv(file_contents, options: {})
139
+ csv_file = try_cache(options, options[:filename]) do
140
+ raw_csv_file = CSVFile.new(file_contents)
141
+ raw_csv_file.parse!
142
+ raw_csv_file
143
+ end
144
+
145
+ csv_file.result.find_all do |dependency|
146
+ dependency[:platform] == platform_name.to_s
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -14,6 +14,8 @@ module Bibliothecary
14
14
  }
15
15
  end
16
16
 
17
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
18
+
17
19
  def self.parse_manifest(file_contents, options: {})
18
20
  json = JSON.parse(file_contents)
19
21
  map_dependencies(json, 'dependencies', 'runtime') +
@@ -17,6 +17,7 @@ module Bibliothecary
17
17
  end
18
18
 
19
19
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
20
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
20
21
 
21
22
  def self.parse_manifest(file_contents, options: {})
22
23
  manifest = Tomlrb.parse(file_contents)
@@ -20,6 +20,8 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
24
+
23
25
  def self.parse_cartfile(file_contents, options: {})
24
26
  map_dependencies(file_contents, 'cartfile')
25
27
  end
@@ -15,6 +15,8 @@ module Bibliothecary
15
15
  }
16
16
  end
17
17
 
18
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
19
+
18
20
  def self.parse_manifest(file_contents, options: {})
19
21
  response = Typhoeus.post("#{Bibliothecary.configuration.clojars_parser_host}/project.clj", body: file_contents)
20
22
  raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.clojars_parser_host}/project.clj", response.response_code) unless response.success?
@@ -33,6 +33,8 @@ module Bibliothecary
33
33
  }
34
34
  end
35
35
 
36
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
37
+
36
38
  def self.parse_podfile_lock(file_contents, options: {})
37
39
  manifest = YAML.load file_contents
38
40
  manifest['PODS'].map do |row|
@@ -27,6 +27,7 @@ module Bibliothecary
27
27
  end
28
28
 
29
29
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
30
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
30
31
 
31
32
  def self.parse_conda(file_contents, options: {})
32
33
  parse_conda_with_kind(file_contents, "manifest")
@@ -19,6 +19,8 @@ module Bibliothecary
19
19
  }
20
20
  end
21
21
 
22
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
+
22
24
  def self.parse_json_manifest(file_contents, options: {})
23
25
  manifest = JSON.parse file_contents
24
26
  manifest['prereqs'].map do |_group, deps|
@@ -17,6 +17,7 @@ module Bibliothecary
17
17
  end
18
18
 
19
19
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
20
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
20
21
 
21
22
  def self.parse_description(file_contents, options: {})
22
23
  manifest = DebControl::ControlFileBase.parse(file_contents)
@@ -20,6 +20,8 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
24
+
23
25
  def self.parse_sdl_manifest(file_contents, options: {})
24
26
  SdlParser.new(:runtime, file_contents).dependencies
25
27
  end
@@ -19,6 +19,8 @@ module Bibliothecary
19
19
  }
20
20
  end
21
21
 
22
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
+
22
24
  def self.parse_json_lock(file_contents, options: {})
23
25
  manifest = JSON.parse file_contents
24
26
  manifest.map do |name, requirement|
@@ -66,6 +66,7 @@ module Bibliothecary
66
66
  end
67
67
 
68
68
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
69
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
69
70
 
70
71
  def self.parse_godep_json(file_contents, options: {})
71
72
  manifest = JSON.parse file_contents
@@ -20,6 +20,7 @@ module Bibliothecary
20
20
  end
21
21
 
22
22
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
23
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
24
 
24
25
  def self.parse_cabal(file_contents, options: {})
25
26
  headers = {
@@ -14,6 +14,9 @@ module Bibliothecary
14
14
  }
15
15
  }
16
16
  end
17
+
18
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
17
19
  end
18
20
  end
19
21
  end
22
+
@@ -19,6 +19,7 @@ module Bibliothecary
19
19
  end
20
20
 
21
21
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
22
23
 
23
24
  def self.parse_mix(file_contents, options: {})
24
25
  response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/", body: file_contents)
@@ -12,6 +12,8 @@ module Bibliothecary
12
12
  }
13
13
  end
14
14
 
15
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
16
+
15
17
  def self.parse_require(file_contents, options: {})
16
18
  deps = []
17
19
  file_contents.split("\n").each do |line|
@@ -18,10 +18,11 @@ module Bibliothecary
18
18
  # An intentionally overly-simplified regex to scrape deps from build.gradle.kts files.
19
19
  # To be truly useful bibliothecary would need a full Kotlin parser that speaks Gradle,
20
20
  # because the Kotlin DSL has many dynamic ways of declaring dependencies.
21
- GRADLE_KTS_SIMPLE_REGEX = /(#{GRADLE_KTS_DEPENDENCY_METHODS.join('|')})\s*\(\s*"([^"]+)"\s*\)/m
22
21
 
23
- # e.g. "group:artifactId:1.2.3"
24
- GRADLE_KTS_GAV_REGEX = /([\w.-]+)\:([\w.-]+)(?:\:([\w.-]+))?/
22
+ GRADLE_KTS_VERSION_REGEX = /[\w.-]+/ # e.g. '1.2.3'
23
+ GRADLE_KTS_INTERPOLATED_VERSION_REGEX = /\$\{.*\}/ # e.g. '${my-project-settings["version"]}'
24
+ GRADLE_KTS_GAV_REGEX = /([\w.-]+)\:([\w.-]+)(?:\:(#{GRADLE_KTS_VERSION_REGEX}|#{GRADLE_KTS_INTERPOLATED_VERSION_REGEX}))?/
25
+ GRADLE_KTS_SIMPLE_REGEX = /(#{GRADLE_KTS_DEPENDENCY_METHODS.join('|')})\s*\(\s*"#{GRADLE_KTS_GAV_REGEX}"\s*\)\s*$/m # e.g. "group:artifactId:1.2.3"
25
26
 
26
27
  MAVEN_PROPERTY_REGEX = /\$\{(.+?)\}/
27
28
  MAX_DEPTH = 5
@@ -83,7 +84,8 @@ module Bibliothecary
83
84
  }
84
85
  end
85
86
 
86
- add_multi_parser Bibliothecary::MultiParsers::CycloneDX
87
+ add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
88
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
87
89
 
88
90
  def self.parse_ivy_manifest(file_contents, options: {})
89
91
  manifest = Ox.parse file_contents
@@ -248,14 +250,13 @@ module Bibliothecary
248
250
 
249
251
  def self.parse_gradle_kts(file_contents, options: {})
250
252
  file_contents
251
- .scan(GRADLE_KTS_SIMPLE_REGEX) # match 'implementation("group:artifactId:version")'
252
- .map { |(_type, dep_match)| GRADLE_KTS_GAV_REGEX.match(dep_match) } # extract ["group", "artifactId", ?"version"]
253
- .reject { |gav_match| gav_match.nil? || gav_match[1].nil? || gav_match[2].nil? } # remove any with missing group/artifactId
254
- .map { |gav_match|
253
+ .scan(GRADLE_KTS_SIMPLE_REGEX) # match 'implementation("group:artifactId:version")'
254
+ .reject { |(_type, group, artifactId, _version)| group.nil? || artifactId.nil? } # remove any matches with missing group/artifactId
255
+ .map { |(type, group, artifactId, version)|
255
256
  {
256
- name: [gav_match[1], gav_match[2]].join(":"),
257
- requirement: gav_match[3] || "*",
258
- type: nil # TODO: we may be able to infer dep types using the _type var above.
257
+ name: [group, artifactId].join(":"),
258
+ requirement: version || "*",
259
+ type: type
259
260
  }
260
261
  }
261
262
  end
@@ -14,6 +14,8 @@ module Bibliothecary
14
14
  }
15
15
  }
16
16
  end
17
+
18
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
17
19
  end
18
20
  end
19
21
  end
@@ -34,6 +34,7 @@ module Bibliothecary
34
34
  end
35
35
 
36
36
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
37
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
37
38
 
38
39
  def self.parse_shrinkwrap(file_contents, options: {})
39
40
  manifest = JSON.parse(file_contents)
@@ -45,6 +45,7 @@ module Bibliothecary
45
45
  end
46
46
 
47
47
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
48
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
48
49
 
49
50
  def self.parse_project_lock_json(file_contents, options: {})
50
51
  manifest = JSON.parse file_contents
@@ -19,6 +19,7 @@ module Bibliothecary
19
19
  end
20
20
 
21
21
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
22
23
 
23
24
  def self.parse_lockfile(file_contents, options: {})
24
25
  manifest = JSON.parse file_contents
@@ -18,6 +18,8 @@ module Bibliothecary
18
18
  }
19
19
  end
20
20
 
21
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
22
+
21
23
  def self.parse_yaml_manifest(file_contents, options: {})
22
24
  manifest = YAML.load file_contents
23
25
  map_dependencies(manifest, 'dependencies', 'runtime') +
@@ -76,6 +76,7 @@ module Bibliothecary
76
76
  end
77
77
 
78
78
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
79
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
79
80
 
80
81
  def self.parse_pipfile(file_contents, options: {})
81
82
  manifest = Tomlrb.parse(file_contents)
@@ -30,6 +30,7 @@ module Bibliothecary
30
30
  end
31
31
 
32
32
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
33
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
33
34
 
34
35
  def self.parse_gemfile_lock(file_contents, options: {})
35
36
  file_contents.lines(chomp: true).map do |line|
@@ -18,6 +18,8 @@ module Bibliothecary
18
18
  }
19
19
  end
20
20
 
21
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
22
+
21
23
  def self.parse_yaml_lockfile(file_contents, options: {})
22
24
  manifest = YAML.load file_contents
23
25
  map_dependencies(manifest, 'shards', 'runtime')
@@ -13,6 +13,7 @@ module Bibliothecary
13
13
  end
14
14
 
15
15
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
16
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
16
17
 
17
18
  def self.parse_package_swift(file_contents, options: {})
18
19
  response = Typhoeus.post("#{Bibliothecary.configuration.swift_parser_host}/to-json", body: file_contents)
@@ -5,28 +5,52 @@ module Bibliothecary
5
5
  attr_reader :manifests
6
6
  attr_reader :lockfiles
7
7
 
8
+ # Create a set of RelatedFilesInfo for the provided file_infos,
9
+ # where each RelatedFilesInfo contains all the file_infos
8
10
  def self.create_from_file_infos(file_infos)
9
11
  returns = []
10
- paths = file_infos.group_by { |info| File.dirname(info.relative_path) }
11
- paths.values.each do |path|
12
- same_pm = path.group_by { |info| info.package_manager}
13
- same_pm.values.each do |value|
14
- returns.append(RelatedFilesInfo.new(value))
12
+
13
+ file_infos_by_directory = file_infos.group_by { |info| File.dirname(info.relative_path) }
14
+ file_infos_by_directory.values.each do |file_infos_for_path|
15
+ file_infos_by_directory_by_package_manager = file_infos_for_path.group_by { |info| info.package_manager}
16
+
17
+ file_infos_by_directory_by_package_manager.values.each do |file_infos_in_directory_for_package_manager|
18
+ returns.append(RelatedFilesInfo.new(file_infos_in_directory_for_package_manager))
15
19
  end
16
20
  end
21
+
17
22
  returns
18
23
  end
19
24
 
20
25
  def initialize(file_infos)
21
26
  package_manager = file_infos.first.package_manager
27
+ ordered_file_infos = file_infos
28
+
22
29
  if package_manager.respond_to?(:lockfile_preference_order)
23
- file_infos = package_manager.lockfile_preference_order(file_infos)
30
+ ordered_file_infos = package_manager.lockfile_preference_order(file_infos)
24
31
  end
32
+
25
33
  @platform = package_manager.platform_name
26
34
  @path = Pathname.new(File.dirname(file_infos.first.relative_path)).cleanpath.to_path
35
+
36
+ @manifests = filter_file_infos_by_package_manager_type(
37
+ file_infos: ordered_file_infos,
38
+ package_manager: package_manager,
39
+ type: "manifest"
40
+ )
41
+
42
+ @lockfiles = filter_file_infos_by_package_manager_type(
43
+ file_infos: ordered_file_infos,
44
+ package_manager: package_manager,
45
+ type: "lockfile"
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ def filter_file_infos_by_package_manager_type(file_infos:, package_manager:, type:)
27
52
  # `package_manager.determine_kind_from_info(info)` can be an Array, so use include? which also works for string
28
- @manifests = file_infos.select { |info| package_manager.determine_kind_from_info(info).include? "manifest" }.map(&:relative_path)
29
- @lockfiles = file_infos.select { |info| package_manager.determine_kind_from_info(info).include? "lockfile" }.map(&:relative_path)
53
+ file_infos.select { |info| package_manager.determine_kind_from_info(info).include?(type) }.map(&:relative_path)
30
54
  end
31
55
  end
32
56
  end
@@ -0,0 +1,67 @@
1
+ module Bibliothecary
2
+ class Runner
3
+ class MultiManifestFilter
4
+ def initialize(path:, related_files_info_entries:, runner:)
5
+ @path = path
6
+ @related_files_info_entries = related_files_info_entries
7
+ @runner = runner
8
+ end
9
+
10
+ # Standalone multi manifest files should *always* be treated as lockfiles,
11
+ # since there's no human-written manifest file to go with them.
12
+ def files_to_check
13
+ @files_to_check ||= @related_files_info_entries.each_with_object({}) do |files_info, all|
14
+ files_info.lockfiles.each do |file|
15
+ all[file] ||= 0
16
+ all[file] += 1
17
+ end
18
+ end
19
+ end
20
+
21
+ def results
22
+ partition_file_entries!
23
+
24
+ no_lockfile_results + single_file_results + multiple_file_results
25
+ end
26
+
27
+ def no_lockfile_results
28
+ @no_lockfile_results ||= @related_files_info_entries.find_all { |rfi| rfi.lockfiles.empty? }
29
+ end
30
+
31
+ def single_file_results
32
+ @single_file_results ||= @single_file_entries.map do |file|
33
+ @related_files_info_entries.find { |rfi| rfi.lockfiles.include?(file) }
34
+ end
35
+ end
36
+
37
+ def multiple_file_results
38
+ return @multiple_file_results if @multiple_file_results
39
+
40
+ @multiple_file_results = []
41
+ @multiple_file_entries.each do |file|
42
+ analysis = @runner.analyse_file(file, File.read(File.join(@path, file)))
43
+
44
+ rfis_for_file = @related_files_info_entries.find_all { |rfi| rfi.lockfiles.include?(file) }
45
+ rfis_for_file.each do |rfi|
46
+ file_analysis = analysis.find { |a| a[:platform] == rfi.platform }
47
+
48
+ next unless file_analysis
49
+ next if file_analysis[:dependencies].empty?
50
+
51
+ @multiple_file_results << rfi
52
+ end
53
+ end
54
+
55
+ @multiple_file_results
56
+ end
57
+
58
+ def partition_file_entries!
59
+ @single_file_entries, @multiple_file_entries = files_to_check.partition { |file, count| count == 1 }
60
+
61
+ @single_file_entries = @single_file_entries.map(&:first)
62
+ @multiple_file_entries = @multiple_file_entries.map(&:first)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -3,7 +3,6 @@ module Bibliothecary
3
3
  # A runner is created every time a file is targeted to be parsed. Don't call
4
4
  # parse methods directory! Use a Runner.
5
5
  class Runner
6
-
7
6
  def initialize(configuration)
8
7
  @configuration = configuration
9
8
  @options = {
@@ -47,9 +46,11 @@ module Bibliothecary
47
46
  Bibliothecary::Parsers.constants.map{|c| Bibliothecary::Parsers.const_get(c) }.sort_by{|c| c.to_s.downcase }
48
47
  end
49
48
 
49
+ # Parses an array of format [{file_path: "", contents: ""},] to match
50
+ # on both filename matches and on content_match patterns.
51
+ #
52
+ # @return [Array<Bibliothecary::FileInfo>] A list of FileInfo, one for each package manager match for each file
50
53
  def load_file_info_list_from_contents(file_path_contents_hash)
51
- # Parses an array of format [{file_path: "", contents: ""},] to match
52
- # on both filename matches, and one content_match patterns.
53
54
  file_list = []
54
55
 
55
56
  file_path_contents_hash.each do |file|
@@ -57,7 +58,7 @@ module Bibliothecary
57
58
 
58
59
  next if ignored_files.include?(info.relative_path)
59
60
 
60
- add_files_to_list(file_list, info)
61
+ add_matching_package_managers_for_file_to_list(file_list, info)
61
62
  end
62
63
 
63
64
  file_list
@@ -71,7 +72,7 @@ module Bibliothecary
71
72
 
72
73
  next if ignored_files.include?(info.relative_path)
73
74
 
74
- add_files_to_list(file_list, info)
75
+ add_matching_package_managers_for_file_to_list(file_list, info)
75
76
  end
76
77
 
77
78
  file_list
@@ -87,12 +88,15 @@ module Bibliothecary
87
88
  next unless FileTest.file?(subpath)
88
89
  next if ignored_files.include?(info.relative_path)
89
90
 
90
- add_files_to_list(file_list, info)
91
+ add_matching_package_managers_for_file_to_list(file_list, info)
91
92
  end
92
93
 
93
94
  file_list
94
95
  end
95
96
 
97
+ # Get a list of files in this path grouped by filename and repeated by package manager.
98
+ #
99
+ # @return [Array<Bibliothecary::RelatedFilesInfo>]
96
100
  def find_manifests(path)
97
101
  RelatedFilesInfo.create_from_file_infos(load_file_info_list(path).reject { |info| info.package_manager.nil? })
98
102
  end
@@ -101,10 +105,16 @@ module Bibliothecary
101
105
  RelatedFilesInfo.create_from_file_infos(load_file_info_list_from_paths(paths).reject { |info| info.package_manager.nil? })
102
106
  end
103
107
 
108
+ # file_path_contents_hash contains an Array of { file_path, contents }
104
109
  def find_manifests_from_contents(file_path_contents_hash)
105
- RelatedFilesInfo.create_from_file_infos(load_file_info_list_from_contents(file_path_contents_hash).reject { |info| info.package_manager.nil? })
110
+ RelatedFilesInfo.create_from_file_infos(
111
+ load_file_info_list_from_contents(
112
+ file_path_contents_hash
113
+ ).reject { |info| info.package_manager.nil? }
114
+ )
106
115
  end
107
116
 
117
+ # Read a manifest file and extract the list of dependencies from that file.
108
118
  def analyse_file(file_path, contents)
109
119
  package_managers.select { |pm| pm.match?(file_path, contents) }.map do |pm|
110
120
  pm.analyse_contents(file_path, contents, options: @options)
@@ -140,15 +150,30 @@ module Bibliothecary
140
150
  @configuration.ignored_files
141
151
  end
142
152
 
153
+ # We don't know what file groups are in multi file manifests until
154
+ # we process them. In those cases, process those, then reject the
155
+ # RelatedFilesInfo objects that aren't in the manifest.
156
+ #
157
+ # This means we're likely analyzing these files twice in processing,
158
+ # but we need that accurate package manager information.
159
+ def filter_multi_manifest_entries(path, related_files_info_entries)
160
+ MultiManifestFilter.new(path: path, related_files_info_entries: related_files_info_entries , runner: self).results
161
+ end
162
+
143
163
  private
144
164
 
145
- def add_files_to_list(file_list, info)
146
- applicable_package_managers(info).each do |package_manager|
147
- file = info.dup
148
- file.package_manager = package_manager
165
+ # Get the list of all package managers that apply to the file provided
166
+ # as file_info, and, for each one, duplicate file_info and fill in
167
+ # the appropriate package manager.
168
+ def add_matching_package_managers_for_file_to_list(file_list, file_info)
169
+ applicable_package_managers(file_info).each do |package_manager|
170
+ new_file_info = file_info.dup
171
+ new_file_info.package_manager = package_manager
149
172
 
150
- file_list.push(file)
173
+ file_list.push(new_file_info)
151
174
  end
152
175
  end
153
176
  end
154
177
  end
178
+
179
+ require_relative './runner/multi_manifest_filter.rb'
@@ -1,3 +1,3 @@
1
1
  module Bibliothecary
2
- VERSION = "8.2.0"
2
+ VERSION = "8.2.3"
3
3
  end
data/lib/bibliothecary.rb CHANGED
@@ -16,6 +16,8 @@ Dir[File.expand_path('../bibliothecary/parsers/*.rb', __FILE__)].each do |file|
16
16
  end
17
17
 
18
18
  module Bibliothecary
19
+ VERSION_OPERATORS = /[~^<>*"]/
20
+
19
21
  def self.analyse(path, ignore_unparseable_files: true)
20
22
  runner.analyse(path, ignore_unparseable_files: ignore_unparseable_files)
21
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bibliothecary
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.2.0
4
+ version: 8.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-29 00:00:00.000000000 Z
11
+ date: 2022-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tomlrb
@@ -260,6 +260,7 @@ files:
260
260
  - lib/bibliothecary/file_info.rb
261
261
  - lib/bibliothecary/multi_parsers/bundler_like_manifest.rb
262
262
  - lib/bibliothecary/multi_parsers/cyclonedx.rb
263
+ - lib/bibliothecary/multi_parsers/dependencies_csv.rb
263
264
  - lib/bibliothecary/multi_parsers/json_runtime.rb
264
265
  - lib/bibliothecary/parsers/bower.rb
265
266
  - lib/bibliothecary/parsers/cargo.rb
@@ -271,7 +272,6 @@ files:
271
272
  - lib/bibliothecary/parsers/cran.rb
272
273
  - lib/bibliothecary/parsers/dub.rb
273
274
  - lib/bibliothecary/parsers/elm.rb
274
- - lib/bibliothecary/parsers/generic.rb
275
275
  - lib/bibliothecary/parsers/go.rb
276
276
  - lib/bibliothecary/parsers/hackage.rb
277
277
  - lib/bibliothecary/parsers/haxelib.rb
@@ -289,6 +289,7 @@ files:
289
289
  - lib/bibliothecary/parsers/swift_pm.rb
290
290
  - lib/bibliothecary/related_files_info.rb
291
291
  - lib/bibliothecary/runner.rb
292
+ - lib/bibliothecary/runner/multi_manifest_filter.rb
292
293
  - lib/bibliothecary/version.rb
293
294
  - lib/sdl_parser.rb
294
295
  homepage: https://github.com/librariesio/bibliothecary
@@ -1,39 +0,0 @@
1
- require 'csv'
2
-
3
- module Bibliothecary
4
- module Parsers
5
- class Generic
6
- include Bibliothecary::Analyser
7
-
8
- def self.mapping
9
- {
10
- match_filename("dependencies.csv") => {
11
- kind: 'lockfile',
12
- parser: :parse_lockfile
13
- }
14
- }
15
- end
16
-
17
- def self.parse_lockfile(file_contents, options: {})
18
- table = CSV.parse(file_contents, headers: true)
19
-
20
- required_headers = ["platform", "name", "requirement"]
21
- missing_headers = required_headers - table.headers
22
- raise "Missing headers #{missing_headers} in CSV" unless missing_headers.empty?
23
-
24
- table.map.with_index do |row, idx|
25
- line = idx + 2 # use 1-based index just like the 'csv' std lib, and count the headers as first row.
26
- required_headers.each do |h|
27
- raise "missing field '#{h}' on line #{line}" if row[h].nil? || row[h].empty?
28
- end
29
- {
30
- platform: row['platform'],
31
- name: row['name'],
32
- requirement: row['requirement'],
33
- type: row.fetch('type', 'runtime'),
34
- }
35
- end
36
- end
37
- end
38
- end
39
- end