bibliothecary 8.1.1 → 8.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bibliothecary/multi_parsers/dependencies_csv.rb +149 -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 +24 -3
  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: 8018773c7a6af0e5027e8239d0f0fdeea8157b208a08620bd943abddb36fa3f5
4
- data.tar.gz: 961b73c80ec0526cfcb3dbc78613a8e907cc01a7638b9b968efb952e785fa5b4
3
+ metadata.gz: 4dd448ab1be90710e81700b68541de4ba3f17731a67a24f734c9ff12d0898d1f
4
+ data.tar.gz: d595b3746c16a87f4442650f0c28ed07f9e4097875b0ca1b646b99914e9c699f
5
5
  SHA512:
6
- metadata.gz: 9b1762248c26af4a3366eebf76fe7160bf86ef4c80018e0aa226a50a48e09821290ccaa1627b60739fe920e93a116498364d3a4ae69b02be6eab7285de2735ab
7
- data.tar.gz: b0e0dec6b219c2d812eb328588540637564f0184195cb003a37beb888e1eb1ae3a398df4c483f8c1ecbd3c7e6922bf385485dd8dc6d08db88bb8e0b873efd24a
6
+ metadata.gz: cd75677e52714d25f33ae3da3295d1146eba3679bb89c093d91840c8c3d8f65bac2365f21021d41e7e58b9d88fa6e4160375870e4fab0d6c4cdd4e753775d68f
7
+ data.tar.gz: c3c70847f495b5c7eb3d0c4fe41454ae50534a4e131b6ef2618d31e3e188147c144363d31d024aab0daef76b62bec9e30f0cdd3d09ef4d61ae793912aaee184d
@@ -0,0 +1,149 @@
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
+ /^version$/i,
43
+ /^(lockfile |)requirement$/i,
44
+ ],
45
+ },
46
+ # Manifests have versions that can have operators.
47
+ "requirement" => {
48
+ match: [
49
+ /^manifest requirement$/i,
50
+ /^version$/i,
51
+ /^(lockfile |)requirement$/i,
52
+ ],
53
+ default: nil
54
+ },
55
+ "type" => {
56
+ default: "runtime",
57
+ match: [
58
+ /^(lockfile |)type$/i,
59
+ /^(manifest |)type$/i
60
+ ]
61
+ }
62
+ }
63
+
64
+ attr_reader :result
65
+
66
+ def initialize(file_contents)
67
+ @file_contents = file_contents
68
+
69
+ @result = nil
70
+
71
+ # A Hash of "our field name" => ["header in CSV file", "lower priority header in CSV file"]
72
+ @header_mappings = {}
73
+ end
74
+
75
+ def parse!
76
+ table = parse_and_validate_csv_file
77
+
78
+ @result = table.map.with_index do |row, idx|
79
+ HEADERS.each_with_object({}) do |(header, info), obj|
80
+ # find the first non-empty field in the row for this header, or nil if not found
81
+ row_data = row[@header_mappings[header]]
82
+
83
+ # some column have default data to fall back on
84
+ if row_data
85
+ obj[header.to_sym] = row_data
86
+ elsif info.has_key?(:default)
87
+ # if the default is nil, don't even add the key to the hash
88
+ obj[header.to_sym] = info[:default] if info[:default]
89
+ else
90
+ # use 1-based index just like the 'csv' std lib, and count the headers as first row.
91
+ raise "Missing required field '#{header}' on line #{idx + 2}."
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def parse_and_validate_csv_file
100
+ table = CSV.parse(@file_contents, headers: true)
101
+
102
+ header_examination_results = map_table_headers_to_local_lookups(table, HEADERS)
103
+ unless header_examination_results[:missing].empty?
104
+ raise "Missing required headers #{header_examination_results[:missing].join(', ')} in CSV. Check to make sure header names are all lowercase."
105
+ end
106
+ @header_mappings = header_examination_results[:found]
107
+
108
+ table
109
+ end
110
+
111
+ def map_table_headers_to_local_lookups(table, local_lookups)
112
+ result = local_lookups.each_with_object({ found: {}, missing: [] }) do |(header, info), obj|
113
+ results = table.headers.each_with_object([]) do |table_header, matches|
114
+ info[:match].each_with_index do |match_regexp, index|
115
+ matches << [table_header, index] if table_header[match_regexp]
116
+ end
117
+ end
118
+
119
+ if results.empty?
120
+ # if a header has a default value it's optional
121
+ obj[:missing] << header unless info.has_key?(:default)
122
+ else
123
+ # select the highest priority header possible
124
+ obj[:found][header] ||= nil
125
+ obj[:found][header] = ([obj[:found][header]] + results).compact.min_by(&:last)
126
+ end
127
+ end
128
+
129
+ # strip off the priorities. only one mapping should remain.
130
+ result[:found].transform_values!(&:first)
131
+
132
+ result
133
+ end
134
+ end
135
+
136
+ def parse_dependencies_csv(file_contents, options: {})
137
+ csv_file = try_cache(options, options[:filename]) do
138
+ raw_csv_file = CSVFile.new(file_contents)
139
+ raw_csv_file.parse!
140
+ raw_csv_file
141
+ end
142
+
143
+ csv_file.result.find_all do |dependency|
144
+ dependency[:platform] == platform_name.to_s
145
+ end
146
+ end
147
+ end
148
+ end
149
+ 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|
@@ -12,6 +12,18 @@ module Bibliothecary
12
12
  # "| \\--- com.google.guava:guava:23.5-jre (*)"
13
13
  GRADLE_DEP_REGEX = /(\+---|\\---){1}/
14
14
 
15
+ # Builtin methods: https://docs.gradle.org/current/userguide/java_plugin.html#tab:configurations
16
+ GRADLE_KTS_DEPENDENCY_METHODS = %w(api compile compileOnlyApi implementation runtimeOnly testCompileOnly testImplementation testRuntimeOnly)
17
+
18
+ # An intentionally overly-simplified regex to scrape deps from build.gradle.kts files.
19
+ # To be truly useful bibliothecary would need a full Kotlin parser that speaks Gradle,
20
+ # because the Kotlin DSL has many dynamic ways of declaring dependencies.
21
+
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"
26
+
15
27
  MAVEN_PROPERTY_REGEX = /\$\{(.+?)\}/
16
28
  MAX_DEPTH = 5
17
29
 
@@ -72,7 +84,8 @@ module Bibliothecary
72
84
  }
73
85
  end
74
86
 
75
- add_multi_parser Bibliothecary::MultiParsers::CycloneDX
87
+ add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
88
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
76
89
 
77
90
  def self.parse_ivy_manifest(file_contents, options: {})
78
91
  manifest = Ox.parse file_contents
@@ -236,8 +249,16 @@ module Bibliothecary
236
249
  end
237
250
 
238
251
  def self.parse_gradle_kts(file_contents, options: {})
239
- # TODO: the gradle-parser side needs to be implemented for this, coming soon.
240
- []
252
+ file_contents
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)|
256
+ {
257
+ name: [group, artifactId].join(":"),
258
+ requirement: version || "*",
259
+ type: type
260
+ }
261
+ }
241
262
  end
242
263
 
243
264
  def self.gradle_dependency_name(group, name)
@@ -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.1.1"
2
+ VERSION = "8.2.2"
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.1.1
4
+ version: 8.2.2
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-28 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