bibliothecary 6.3.1 → 6.3.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba88fac4e7fbc938bf270504f1b41787f314105f237bb1e4e080e5ad82379aef
4
- data.tar.gz: 28de14e318ae6684074ac38e8a7b2f1fb57fa83d21a97e7908d6afe110bb5d22
3
+ metadata.gz: e4a0567c8ec55a4efdce0ef3582ffb085587a4efcc50b91e3f3596444629d60c
4
+ data.tar.gz: 63c5203b34b171ef99ce8d9beac0e05aabc2fcb202c1125a3184a2968ba6faba
5
5
  SHA512:
6
- metadata.gz: a07ec3aeb787fe83e7a4d415f83b5aeefe1649eb590a320a48a90053b26b5bf215e6649799bc5792e9e20e5e6fc920bddd0f43c3e8f08111d34baf2695f64ed9
7
- data.tar.gz: 3da7fe10d6c6e036091a82e84a1cbfa80ef9ee58f37289629fe8076b1c49da38a7dc40d03dfe7b56b3e1a12327dc714bf220576a2ca6b072ab66a7934bf06c0d
6
+ metadata.gz: 4e68baf27de703a993167504612de962db9ae01b8aa8ab84e2de8556074521076fe7ea27dc0e5596c1188dfd747bb541a31898da5c6ca6f2e40d0af2e7adbd09
7
+ data.tar.gz: 25f3d2f9f38bcce1c339851e5b68b1f92f354ecd0b4ea0608f258c69c7a6a0a0376dd4f2e160208c537aa1370aa0298680af6d10c22c4e82c89a14936ed9aa69
@@ -0,0 +1,2 @@
1
+ ci:
2
+ extra_ignore_directories: [ spec/fixtures ]
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.4.2
4
3
  - 2.5.1
5
4
  cache: bundler
6
5
  before_install:
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency "commander"
28
28
 
29
29
  spec.add_development_dependency "bundler", "~> 1.11"
30
+ spec.add_development_dependency "pry"
30
31
  spec.add_development_dependency "rake", "~> 12.0"
31
32
  spec.add_development_dependency "rspec", "~> 3.0"
32
33
  spec.add_development_dependency "webmock"
@@ -1,55 +1,71 @@
1
1
  require "bibliothecary/version"
2
2
  require "bibliothecary/analyser"
3
3
  require "bibliothecary/configuration"
4
+ require "bibliothecary/runner"
5
+ require "bibliothecary/exceptions"
6
+ require "bibliothecary/file_info"
7
+ require "find"
4
8
 
5
9
  Dir[File.expand_path('../bibliothecary/parsers/*.rb', __FILE__)].each do |file|
6
10
  require file
7
11
  end
8
12
 
9
13
  module Bibliothecary
10
- def self.analyse(path)
11
- cmd = `find #{path} -type f | grep -vE "#{ignored_files_regex}"`
12
- file_list = cmd.split("\n").sort
13
- package_managers.map{|pm| pm.analyse(path, file_list) }.flatten.compact
14
+ def self.analyse(path, ignore_unparseable_files: true)
15
+ runner.analyse(path, ignore_unparseable_files: ignore_unparseable_files)
16
+ end
17
+
18
+ # deprecated; use load_file_info_list.
19
+ def self.load_file_list(path)
20
+ runner.load_file_list(path)
21
+ end
22
+
23
+ def self.init_package_manager(info)
24
+ runner.init_package_manager(info)
25
+ end
26
+
27
+ def self.load_file_info_list(path)
28
+ runner.load_file_info_list(path)
14
29
  end
15
30
 
16
31
  def self.analyse_file(file_path, contents)
17
- package_managers.map do |pm|
18
- pm.analyse_contents(file_path, contents)
19
- end.flatten.uniq.compact
32
+ runner.analyse_file(file_path, contents)
20
33
  end
21
34
 
22
35
  def self.identify_manifests(file_list)
23
- allowed_file_list = file_list.reject{|f| f.start_with?(*ignored_files) }
24
- package_managers.map do |pm|
25
- allowed_file_list.select do |file_path|
26
- pm.match?(file_path)
27
- end
28
- end.flatten.uniq.compact
36
+ runner.identify_manifests(file_list)
29
37
  end
30
38
 
31
39
  def self.package_managers
32
- Bibliothecary::Parsers.constants.map{|c| Bibliothecary::Parsers.const_get(c) }.sort_by{|c| c.to_s.downcase }
40
+ runner.package_managers
33
41
  end
34
42
 
35
- def self.ignored_files
36
- configuration.ignored_files
43
+ def self.ignored_dirs
44
+ configuration.ignored_dirs
37
45
  end
38
46
 
39
- def self.ignored_files_regex
40
- ignored_files.join('|')
47
+ def self.ignored_files
48
+ configuration.ignored_files
41
49
  end
42
50
 
43
51
  class << self
44
52
  attr_writer :configuration
45
53
  end
46
54
 
55
+ def self.runner
56
+ configuration
57
+ @runner
58
+ end
59
+
47
60
  def self.configuration
48
61
  @configuration ||= Configuration.new
62
+ @runner = Runner.new(@configuration)
63
+ @configuration
49
64
  end
50
65
 
51
66
  def self.reset
52
67
  @configuration = Configuration.new
68
+ @runner = Runner.new(@configuration)
53
69
  end
54
70
 
55
71
  def self.configure
@@ -1,20 +1,81 @@
1
1
  module Bibliothecary
2
2
  module Analyser
3
+ def self.create_error_analysis(platform_name, relative_path, kind, message)
4
+ {
5
+ platform: platform_name,
6
+ path: relative_path,
7
+ dependencies: nil,
8
+ kind: kind,
9
+ success: false,
10
+ error_message: message
11
+ }
12
+ end
13
+
14
+ def self.create_analysis(platform_name, relative_path, kind, dependencies)
15
+ {
16
+ platform: platform_name,
17
+ path: relative_path,
18
+ dependencies: dependencies,
19
+ kind: kind,
20
+ success: true
21
+ }
22
+ end
23
+
3
24
  def self.included(base)
4
25
  base.extend(ClassMethods)
5
26
  end
6
27
  module ClassMethods
28
+ def mapping_entry_match?(regex, details, info)
29
+ if info.relative_path.match(regex)
30
+ # we only want to load contents if we don't have them already
31
+ # and there's a content_matcher method to use
32
+ return true if details[:content_matcher].nil?
33
+ # this is the libraries.io case where we won't load all .xml
34
+ # files (for example) just to look at their contents, we'll
35
+ # assume they are not manifests.
36
+ return false if info.contents.nil?
37
+ return send(details[:content_matcher], info.contents)
38
+ else
39
+ return false
40
+ end
41
+ end
42
+
7
43
  def parse_file(filename, contents)
8
44
  mapping.each do |regex, details|
9
- if filename.match(regex)
10
- return send(details[:parser], contents)
45
+ if mapping_entry_match?(regex, details, FileInfo.new(nil, filename, contents))
46
+ begin
47
+ # The `parser` method should raise an exception if the file is malformed,
48
+ # should return empty [] if the file is fine but simply doesn't contain
49
+ # any dependencies, and should never return nil. At the time of writing
50
+ # this comment, some of the parsers return [] or nil to mean an error
51
+ # which is confusing to users.
52
+ return send(details[:parser], contents)
53
+ rescue Exception => e # default is StandardError but C bindings throw Exceptions
54
+ # the C xml parser also puts a newline at the end of the message
55
+ raise Bibliothecary::FileParsingError.new(e.message.strip, filename)
56
+ end
11
57
  end
12
58
  end
13
- return []
59
+ # this can be raised if we don't check match?/match_info?,
60
+ # OR don't have the file contents when we check them, so
61
+ # it turns out for example that a .xml file isn't a
62
+ # manifest after all.
63
+ raise Bibliothecary::FileParsingError.new("No parser for this file type", filename)
64
+ end
65
+
66
+ # this is broken with contents=nil because it can't look at file
67
+ # contents, so skips manifests that are ambiguously a
68
+ # manifest considering only the filename. However, those are
69
+ # the semantics that libraries.io uses since it doesn't have
70
+ # the files locally.
71
+ def match?(filename, contents = nil)
72
+ match_info?(FileInfo.new(nil, filename, contents))
14
73
  end
15
74
 
16
- def match?(filename)
17
- mapping.keys.any?{|regex| filename.match(regex) }
75
+ def match_info?(info)
76
+ mapping.any? do |regex, details|
77
+ mapping_entry_match?(regex, details, info)
78
+ end
18
79
  end
19
80
 
20
81
  def platform_name
@@ -43,15 +104,15 @@ module Bibliothecary
43
104
 
44
105
  def set_related_paths_field(by_dirname_dest, by_dirname_source)
45
106
  by_dirname_dest.each do |dirname, analyses|
46
- analyses.each do |analysis|
47
- source_analyses = by_dirname_source[dirname].map { |source_analysis| source_analysis[:path] }
107
+ analyses.each do |(info, analysis)|
108
+ source_analyses = by_dirname_source[dirname].map { |(info, source_analysis)| info.relative_path }
48
109
  analysis[:related_paths] = source_analyses.sort
49
110
  end
50
111
  end
51
112
  end
52
113
 
53
114
  def add_related_paths(analyses)
54
- analyses.each do |analysis|
115
+ analyses.each do |(info, analysis)|
55
116
  analysis[:related_paths] = []
56
117
  end
57
118
 
@@ -67,13 +128,16 @@ module Bibliothecary
67
128
  "lockfile" => Hash.new { |h, k| h[k] = [] }
68
129
  }
69
130
 
70
- analyses.each do |analysis|
71
- dirname = File.dirname(analysis[:path])
72
- by_dirname[analysis[:kind]][dirname].push(analysis)
131
+ analyses.each do |(info, analysis)|
132
+ dirname = File.dirname(info.relative_path)
133
+ by_dirname[analysis[:kind]][dirname].push([info, analysis])
73
134
  end
74
135
 
75
136
  by_dirname["manifest"].each do |_, manifests|
76
- manifests.delete_if { |manifest| !determine_can_have_lockfile(manifest[:path]) }
137
+ # This determine_can_have_lockfile in theory needs the file contents but
138
+ # in practice doesn't right now since the only mapping that needs
139
+ # file contents is a lockfile and not a manifest so won't reach here.
140
+ manifests.delete_if { |(info, manifest)| !determine_can_have_lockfile_from_info(info) }
77
141
  end
78
142
 
79
143
  set_related_paths_field(by_dirname["manifest"], by_dirname["lockfile"])
@@ -81,44 +145,60 @@ module Bibliothecary
81
145
  end
82
146
 
83
147
  def analyse(folder_path, file_list)
84
- analyses = file_list.map do |path|
85
- filename = path.gsub(folder_path, '').gsub(/^\//, '')
86
- next unless match?(filename)
87
- contents = File.open(path).read
88
- analyse_contents(filename, contents)
89
- end.compact
148
+ analyse_file_info(file_list.map { |full_path| FileInfo.new(folder_path, full_path) })
149
+ end
150
+
151
+ def analyse_file_info(file_info_list)
152
+ analyses = file_info_list.map do |info|
153
+ next unless match_info?(info)
154
+ [info, analyse_contents_from_info(info)]
155
+ end
156
+
157
+ # strip the ones we failed to analyse
158
+ analyses = analyses.reject { |(info, analysis)| analysis.nil? }
90
159
 
91
160
  add_related_paths(analyses)
92
161
 
93
- analyses
162
+ analyses.map { |(info, analysis)| analysis }
94
163
  end
95
164
 
96
165
  def analyse_contents(filename, contents)
97
- dependencies = parse_file(filename, contents)
98
- if dependencies && dependencies.any?
99
- {
100
- platform: platform_name,
101
- path: filename,
102
- dependencies: dependencies,
103
- kind: determine_kind(filename)
104
- }
105
- end
106
- # rescue
107
- # nil
166
+ analyse_contents_from_info(FileInfo.new(nil, filename, contents))
167
+ end
168
+
169
+ def analyse_contents_from_info(info)
170
+ kind = determine_kind_from_info(info)
171
+ dependencies = parse_file(info.relative_path, info.contents)
172
+
173
+ Bibliothecary::Analyser::create_analysis(platform_name, info.relative_path, kind, dependencies || [])
174
+ rescue Bibliothecary::FileParsingError => e
175
+ Bibliothecary::Analyser::create_error_analysis(platform_name, info.relative_path, kind, e.message)
176
+ end
177
+
178
+ # calling this with contents=nil can produce less-informed
179
+ # results, but kept for back compat
180
+ def determine_kind(filename, contents = nil)
181
+ determine_kind_from_info(FileInfo.new(nil, filename, contents))
108
182
  end
109
183
 
110
- def determine_kind(filename)
184
+ def determine_kind_from_info(info)
111
185
  mapping.each do |regex, details|
112
- if filename.match(regex)
186
+ if mapping_entry_match?(regex, details, info)
113
187
  return details[:kind]
114
188
  end
115
189
  end
116
- return []
190
+ return nil
191
+ end
192
+
193
+ # calling this with contents=nil can produce less-informed
194
+ # results, but kept for back compat
195
+ def determine_can_have_lockfile(filename, contents = nil)
196
+ determine_can_have_lockfile_from_info(FileInfo.new(nil, filename, contents))
117
197
  end
118
198
 
119
- def determine_can_have_lockfile(filename)
199
+ def determine_can_have_lockfile_from_info(info)
120
200
  mapping.each do |regex, details|
121
- if filename.match(regex)
201
+ if mapping_entry_match?(regex, details, info)
122
202
  return details.fetch(:can_have_lockfile, true)
123
203
  end
124
204
  end
@@ -1,5 +1,6 @@
1
1
  module Bibliothecary
2
2
  class Configuration
3
+ attr_accessor :ignored_dirs
3
4
  attr_accessor :ignored_files
4
5
  attr_accessor :carthage_parser_host
5
6
  attr_accessor :clojars_parser_host
@@ -10,7 +11,8 @@ module Bibliothecary
10
11
  attr_accessor :cabal_parser_host
11
12
 
12
13
  def initialize
13
- @ignored_files = ['.git', 'node_modules', 'bower_components', 'spec/fixtures', 'vendor', 'dist']
14
+ @ignored_dirs = ['.git', 'node_modules', 'bower_components', 'vendor', 'dist']
15
+ @ignored_files = []
14
16
  @carthage_parser_host = 'https://carthage.libraries.io'
15
17
  @clojars_parser_host = 'https://clojars.libraries.io'
16
18
  @mix_parser_host = 'https://mix.libraries.io'
@@ -0,0 +1,18 @@
1
+ module Bibliothecary
2
+ class RemoteParsingError < StandardError
3
+ attr_accessor :code
4
+ def initialize(msg, code)
5
+ @code = code
6
+ super(msg)
7
+ end
8
+ end
9
+
10
+ class FileParsingError < StandardError
11
+ attr_accessor :filename
12
+ def initialize(msg, filename)
13
+ @filename = filename
14
+ msg = "#{filename}: #{msg}" unless msg.include?(filename)
15
+ super("#{msg}")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ require 'pathname'
2
+
3
+ module Bibliothecary
4
+ class FileInfo
5
+ attr_reader :folder_path
6
+ attr_reader :relative_path
7
+ attr_reader :full_path
8
+ attr_accessor :package_manager
9
+
10
+ def contents
11
+ @contents ||=
12
+ begin
13
+ if @folder_path.nil?
14
+ # if we have no folder_path then we aren't dealing with a
15
+ # file that's actually on the filesystem
16
+ nil
17
+ else
18
+ File.open(@full_path).read
19
+ end
20
+ end
21
+ end
22
+
23
+ # If the FileInfo represents an actual file on disk,
24
+ # the contents can be nil and lazy-loaded; we allow
25
+ # contents to be passed in here to allow pulling them
26
+ # from somewhere other than the disk.
27
+ def initialize(folder_path, full_path, contents = nil)
28
+ # Note that cleanpath does NOT touch the filesystem,
29
+ # leaving the lazy-load of file contents as the only
30
+ # time we touch the filesystem.
31
+
32
+ full_pathname = Pathname.new(full_path)
33
+ @full_path = full_pathname.cleanpath.to_path
34
+
35
+ if folder_path.nil?
36
+ # this is the case where we e.g. have filenames from the GitHub API
37
+ # and don't have a local folder
38
+ @folder_path = nil
39
+ @relative_path = @full_path
40
+ else
41
+ folder_pathname = Pathname.new(folder_path)
42
+ @folder_path = folder_pathname.cleanpath.to_path
43
+ @relative_path = full_pathname.relative_path_from(folder_pathname).cleanpath.to_path
44
+ end
45
+
46
+ @contents = contents
47
+
48
+ @package_manager = nil
49
+ end
50
+ end
51
+ end
@@ -34,6 +34,7 @@ module Bibliothecary
34
34
 
35
35
  def self.map_dependencies(manifest, path)
36
36
  response = Typhoeus.post("#{Bibliothecary.configuration.carthage_parser_host}/#{path}", params: {body: manifest})
37
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.carthage_parser_host}/#{path}", response.response_code) unless response.success?
37
38
  json = JSON.parse(response.body)
38
39
 
39
40
  json.map do |dependency|
@@ -17,6 +17,7 @@ module Bibliothecary
17
17
 
18
18
  def self.parse_manifest(manifest)
19
19
  response = Typhoeus.post("#{Bibliothecary.configuration.clojars_parser_host}/project.clj", body: manifest)
20
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.clojars_parser_host}/project.clj", response.response_code) unless response.success?
20
21
  json = JSON.parse response.body
21
22
  index = json.index("dependencies")
22
23
 
@@ -26,11 +26,8 @@ module Bibliothecary
26
26
 
27
27
  response = Typhoeus.post("#{Bibliothecary.configuration.cabal_parser_host}/parse", headers: headers, body: file_contents)
28
28
 
29
- if response.response_code == 200 then
30
- JSON.parse(response.body, symbolize_names: true)
31
- else
32
- []
33
- end
29
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.cabal_parser_host}/parse", response.response_code) unless response.success?
30
+ JSON.parse(response.body, symbolize_names: true)
34
31
  end
35
32
 
36
33
  def self.parse_cabal_config(file_contents)
@@ -20,6 +20,7 @@ module Bibliothecary
20
20
 
21
21
  def self.parse_mix(manifest)
22
22
  response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/", body: manifest)
23
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.mix_parser_host}/", response.response_code) unless response.success?
23
24
  json = JSON.parse response.body
24
25
 
25
26
  json.map do |name, version|
@@ -33,6 +34,7 @@ module Bibliothecary
33
34
 
34
35
  def self.parse_mix_lock(manifest)
35
36
  response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/lock", body: manifest)
37
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.mix_parser_host}/", response.response_code) unless response.success?
36
38
  json = JSON.parse response.body
37
39
 
38
40
  json.map do |name, info|
@@ -5,6 +5,12 @@ module Bibliothecary
5
5
  class Maven
6
6
  include Bibliothecary::Analyser
7
7
 
8
+ # e.g. "annotationProcessor - Annotation processors and their dependencies for source set 'main'."
9
+ GRADLE_TYPE_REGEX = /^(\w+)/
10
+
11
+ # "| \\--- com.google.guava:guava:23.5-jre (*)"
12
+ GRADLE_DEP_REGEX = /(\+---|\\---){1}/
13
+
8
14
  def self.mapping
9
15
  {
10
16
  /^ivy\.xml$|.*\/ivy\.xml$/i => {
@@ -18,6 +24,15 @@ module Bibliothecary
18
24
  /^build.gradle$|.*\/build.gradle$/i => {
19
25
  kind: 'manifest',
20
26
  parser: :parse_gradle
27
+ },
28
+ /^.+.xml$/i => {
29
+ content_matcher: :ivy_report?,
30
+ kind: 'lockfile',
31
+ parser: :parse_ivy_report
32
+ },
33
+ /^gradle-dependencies-q\.txt|.*\/gradle-dependencies-q\.txt$/i => {
34
+ kind: 'lockfile',
35
+ parser: :parse_gradle_resolved
21
36
  }
22
37
  }
23
38
  end
@@ -32,8 +47,66 @@ module Bibliothecary
32
47
  type: 'runtime'
33
48
  }
34
49
  end
35
- rescue
36
- []
50
+ end
51
+
52
+ def self.ivy_report?(file_contents)
53
+ doc = Ox.parse file_contents
54
+ root = doc&.locate("ivy-report")&.first
55
+ return !root.nil?
56
+ rescue Exception => e
57
+ # We rescue exception here since native libs can throw a non-StandardError
58
+ # We don't want to throw errors during the matching phase, only during
59
+ # parsing after we match.
60
+ false
61
+ end
62
+
63
+ def self.parse_ivy_report(file_contents)
64
+ doc = Ox.parse file_contents
65
+ root = doc.locate("ivy-report").first
66
+ raise "ivy-report document does not have ivy-report at the root" if root.nil?
67
+ info = doc.locate("ivy-report/info").first
68
+ raise "ivy-report document lacks <info> element" if info.nil?
69
+ type = info.attributes[:conf]
70
+ type = "unknown" if type.nil?
71
+ modules = doc.locate("ivy-report/dependencies/module")
72
+ modules.map do |mod|
73
+ attrs = mod.attributes
74
+ org = attrs[:organisation]
75
+ name = attrs[:name]
76
+ version = mod.locate('revision').first&.attributes[:name]
77
+
78
+ next nil if org.nil? or name.nil? or version.nil?
79
+
80
+ {
81
+ name: "#{org}:#{name}",
82
+ requirement: version,
83
+ type: type
84
+ }
85
+ end.compact
86
+ end
87
+
88
+ def self.parse_gradle_resolved(file_contents)
89
+ type = nil
90
+ file_contents.split("\n").map do |line|
91
+ type_match = GRADLE_TYPE_REGEX.match(line)
92
+ type = type_match.captures[0] if type_match
93
+
94
+ gradle_dep_match = GRADLE_DEP_REGEX.match(line)
95
+ next unless gradle_dep_match
96
+
97
+ split = gradle_dep_match.captures[0]
98
+
99
+ # org.springframework.boot:spring-boot-starter-web:2.1.0.M3 (*)
100
+ # Lines can end with (n) or (*) to indicate that something was not resolved (n) or resolved previously (*).
101
+ dep = line.split(split)[1].sub(/\(n\)$/, "").sub(/\(\*\)$/,"").strip.split(":")
102
+ version = dep[-1]
103
+ version = version.split("->")[-1].strip if line.include?("->")
104
+ {
105
+ name: dep[0, dep.length - 1].join(":"),
106
+ requirement: version,
107
+ type: type
108
+ }
109
+ end.compact.uniq {|item| [item[:name], item[:requirement], item[:type]]}
37
110
  end
38
111
 
39
112
  def self.parse_pom_manifest(file_contents)
@@ -51,12 +124,11 @@ module Bibliothecary
51
124
  type: extract_pom_dep_info(xml, dependency, 'scope') || 'runtime'
52
125
  }
53
126
  end
54
- rescue
55
- []
56
127
  end
57
128
 
58
129
  def self.parse_gradle(manifest)
59
130
  response = Typhoeus.post("#{Bibliothecary.configuration.gradle_parser_host}/parse", body: manifest)
131
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.gradle_parser_host}/parse", response.response_code) unless response.success?
60
132
  json = JSON.parse(response.body)
61
133
  return [] unless json['dependencies']
62
134
  json['dependencies'].map do |dependency|
@@ -40,23 +40,31 @@ module Bibliothecary
40
40
  def self.parse_package_lock(file_contents)
41
41
  manifest = JSON.parse(file_contents)
42
42
  manifest.fetch('dependencies',[]).map do |name, requirement|
43
+ if requirement.fetch("dev", false)
44
+ type = 'development'
45
+ else
46
+ type = 'runtime'
47
+ end
43
48
  {
44
49
  name: name,
45
50
  requirement: requirement["version"],
46
- type: 'runtime'
51
+ type: type
47
52
  }
48
53
  end
49
54
  end
50
55
 
51
56
  def self.parse_manifest(file_contents)
52
57
  manifest = JSON.parse(file_contents)
58
+ raise "appears to be a lockfile rather than manifest format" if manifest.key?('lockfileVersion')
53
59
  map_dependencies(manifest, 'dependencies', 'runtime') +
54
60
  map_dependencies(manifest, 'devDependencies', 'development')
55
61
  end
56
62
 
57
63
  def self.parse_yarn_lock(file_contents)
58
64
  response = Typhoeus.post("#{Bibliothecary.configuration.yarn_parser_host}/parse", body: file_contents)
59
- return [] unless response.response_code == 200
65
+
66
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.yarn_parser_host}/parse", response.response_code) unless response.success?
67
+
60
68
  json = JSON.parse(response.body, symbolize_names: true)
61
69
  json.uniq.map do |dep|
62
70
  {
@@ -52,7 +52,7 @@ module Bibliothecary
52
52
  manifest.packages.locate('package').map do |dependency|
53
53
  {
54
54
  name: dependency.id,
55
- requirement: dependency.version,
55
+ requirement: (dependency.version if dependency.respond_to? "version") || "*",
56
56
  type: 'runtime'
57
57
  }
58
58
  end
@@ -65,7 +65,7 @@ module Bibliothecary
65
65
  packages = manifest.locate('ItemGroup/PackageReference').map do |dependency|
66
66
  {
67
67
  name: dependency.Include,
68
- requirement: dependency.Version,
68
+ requirement: (dependency.Version if dependency.respond_to? "Version") || "*",
69
69
  type: 'runtime'
70
70
  }
71
71
  end
@@ -14,6 +14,7 @@ module Bibliothecary
14
14
 
15
15
  def self.parse_package_swift(manifest)
16
16
  response = Typhoeus.post("#{Bibliothecary.configuration.swift_parser_host}/to-json", body: manifest)
17
+ raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.swift_parser_host}/to-json", response.response_code) unless response.success?
17
18
  json = JSON.parse(response.body)
18
19
  json["dependencies"].map do |dependency|
19
20
  name = dependency['url'].gsub(/^https?:\/\//, '').gsub(/\.git$/,'')
@@ -0,0 +1,98 @@
1
+ module Bibliothecary
2
+ # A class that allows bibliothecary to run with multiple configurations at once, rather than with one global
3
+ class Runner
4
+
5
+ def initialize(configuration)
6
+ @configuration = configuration
7
+ end
8
+
9
+ def analyse(path, ignore_unparseable_files: true)
10
+ info_list = load_file_info_list(path)
11
+
12
+ info_list = info_list.reject { |info| info.package_manager.nil? } if ignore_unparseable_files
13
+
14
+ # Each package manager needs to see its entire list so it can
15
+ # associate related manifests and lockfiles for example.
16
+ analyses = package_managers.map do |pm|
17
+ matching_infos = info_list.select { |info| info.package_manager == pm }
18
+ pm.analyse_file_info(matching_infos)
19
+ end
20
+ analyses = analyses.flatten.compact
21
+
22
+ info_list.select { |info| info.package_manager.nil? }.each do |info|
23
+ analyses.push(Bibliothecary::Analyser::create_error_analysis('unknown', info.relative_path, 'unknown',
24
+ 'No parser for this file type'))
25
+ end
26
+
27
+ analyses
28
+ end
29
+
30
+ # deprecated; use load_file_info_list.
31
+ def load_file_list(path)
32
+ load_file_info_list(path).map { |info| info.full_path }
33
+ end
34
+
35
+ def init_package_manager(info)
36
+ # set the package manager on each info
37
+ matches = package_managers.select { |pm| pm.match_info?(info) }
38
+
39
+ info.package_manager = matches[0] if matches.length == 1
40
+
41
+ # this is a bug at the moment if it's raised (we don't handle it sensibly)
42
+ raise "Multiple package managers fighting over #{info.relative_path}: #{matches.map(&:to_s)}" if matches.length > 1
43
+ end
44
+
45
+ def package_managers
46
+ Bibliothecary::Parsers.constants.map{|c| Bibliothecary::Parsers.const_get(c) }.sort_by{|c| c.to_s.downcase }
47
+ end
48
+
49
+ def load_file_info_list(path)
50
+ file_list = []
51
+ Find.find(path) do |subpath|
52
+ info = FileInfo.new(path, subpath)
53
+ Find.prune if FileTest.directory?(subpath) && ignored_dirs.include?(info.relative_path)
54
+ next unless FileTest.file?(subpath)
55
+ next if ignored_files.include?(info.relative_path)
56
+
57
+ init_package_manager(info)
58
+
59
+ file_list.push(info)
60
+ end
61
+ file_list
62
+ end
63
+
64
+ def analyse_file(file_path, contents)
65
+ package_managers.select { |pm| pm.match?(file_path, contents) }.map do |pm|
66
+ pm.analyse_contents(file_path, contents)
67
+ end.flatten.uniq.compact
68
+ end
69
+
70
+ # this skips manifests sometimes because it doesn't look at file
71
+ # contents and can't establish from only regexes that the thing
72
+ # is a manifest. We exclude rather than include ambiguous filenames
73
+ # because this API is used by libraries.io and we don't want to
74
+ # download all .xml files from GitHub.
75
+ def identify_manifests(file_list)
76
+ ignored_dirs_with_slash = ignored_dirs.map { |d| if d.end_with?("/") then d else d + "/" end }
77
+ allowed_file_list = file_list.reject do |f|
78
+ ignored_dirs.include?(f) || f.start_with?(*ignored_dirs_with_slash)
79
+ end
80
+ allowed_file_list = allowed_file_list.reject{|f| ignored_files.include?(f)}
81
+ package_managers.map do |pm|
82
+ allowed_file_list.select do |file_path|
83
+ # this is a call to match? without file contents, which will skip
84
+ # ambiguous filenames that are only possibly a manifest
85
+ pm.match?(file_path)
86
+ end
87
+ end.flatten.uniq.compact
88
+ end
89
+
90
+ def ignored_dirs
91
+ @configuration.ignored_dirs
92
+ end
93
+
94
+ def ignored_files
95
+ @configuration.ignored_files
96
+ end
97
+ end
98
+ end
@@ -1,3 +1,3 @@
1
1
  module Bibliothecary
2
- VERSION = "6.3.1"
2
+ VERSION = "6.3.2"
3
3
  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: 6.3.1
4
+ version: 6.3.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: 2018-07-12 00:00:00.000000000 Z
11
+ date: 2019-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: toml-rb
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '1.11'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: rake
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -190,13 +204,11 @@ extra_rdoc_files: []
190
204
  files:
191
205
  - ".codeclimate.yml"
192
206
  - ".github/CONTRIBUTING.md"
193
- - ".github/ISSUE_TEMPLATE.md"
194
- - ".github/PULL_REQUEST_TEMPLATE.md"
195
- - ".github/SUPPORT.md"
196
207
  - ".gitignore"
197
208
  - ".rspec"
198
209
  - ".rubocop.yml"
199
210
  - ".ruby-version"
211
+ - ".tidelift.yml"
200
212
  - ".travis.yml"
201
213
  - CODE_OF_CONDUCT.md
202
214
  - Gemfile
@@ -212,6 +224,8 @@ files:
212
224
  - lib/bibliothecary/analyser.rb
213
225
  - lib/bibliothecary/cli.rb
214
226
  - lib/bibliothecary/configuration.rb
227
+ - lib/bibliothecary/exceptions.rb
228
+ - lib/bibliothecary/file_info.rb
215
229
  - lib/bibliothecary/parsers/bower.rb
216
230
  - lib/bibliothecary/parsers/cargo.rb
217
231
  - lib/bibliothecary/parsers/carthage.rb
@@ -236,6 +250,7 @@ files:
236
250
  - lib/bibliothecary/parsers/rubygems.rb
237
251
  - lib/bibliothecary/parsers/shard.rb
238
252
  - lib/bibliothecary/parsers/swift_pm.rb
253
+ - lib/bibliothecary/runner.rb
239
254
  - lib/bibliothecary/version.rb
240
255
  - lib/sdl_parser.rb
241
256
  homepage: https://github.com/librariesio/bibliothecary
@@ -1,18 +0,0 @@
1
- Thanks for taking the time to raise an issue. This template should guide you through the process of submitting a bug, enhancement or feature request. Please erase any part of this template that is not relevant to your issue.
2
-
3
- ## Bugs
4
- Before submitting a bug report:
5
-
6
- - [ ] Double-check that the bug is persistent,
7
- - [ ] Double-check the bug hasn't already been reported [on our issue tracker](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+org%3Alibrariesio), they *should* be labelled `bug` or `bugsnag`.
8
-
9
- If you have completed those steps then please replace this section with a description of the steps taken to recreate the bug, the expected behavior and the observed behavior.
10
-
11
- ## Enhancements and Features
12
-
13
- Before submitting an enhancement or feature request:
14
-
15
- - [ ] Check that the enhancement is not already [in our issue tracker](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+org%3Alibrariesio), they should be labelled 'enhancement'.,
16
- - [ ] For large feature requests, check that your request aligns with our strategy http://docs.libraries.io/strategy.
17
-
18
- If you have complete the above step then please replace this section with a description of your proposed enhancement or feature, the motivation for it, an approach and any alternative approaches considered, and whether you are willing and able to create a pull request for it. Note that we may close this issue if it's not something we're planning on working on.
@@ -1,10 +0,0 @@
1
- Thanks taking the time to contribute. This template should help guide you through the process of creating a pull request for review. Please erase any part of this template that is not relevant to your pull request:
2
-
3
-
4
- - [ ] Have you followed the guidelines for [contributors](http://docs.libraries.io/contributorshandbook)?
5
- - [ ] Have you checked to ensure there aren't other open pull requests on the repository for a similar change?
6
- - [ ] Is there a corresponding ticket for your pull request?
7
- - [ ] Have you written new tests for your changes?
8
- - [ ] Have you successfully run the project with your changes locally?
9
-
10
- If so then please replace this section with a link to the ticket(s) it addressed, an explanation of your change and why you think we should include it. Thanks again!
@@ -1,10 +0,0 @@
1
- # Libraries.io Support
2
-
3
- If you're looking for support for Libraries.io there are a lot of options, check out:
4
-
5
- * Documentation &mdash; https://docs.libraries.io
6
- * Email &mdash; support@libraries.io
7
- * Twitter &mdash; https://twitter.com/librariesio
8
- * Chat &mdash; https://slack.libraries.io
9
-
10
- On Discuss and in the Libraries.io Slack team, there are a bunch of helpful community members that should be willing to point you in the right direction.