license_finder 7.0.1 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +7 -0
  3. data/.pre-commit-hooks.yaml +10 -0
  4. data/.rubocop.yml +5 -1
  5. data/CHANGELOG.md +41 -0
  6. data/CONTRIBUTING.md +1 -0
  7. data/Dockerfile +129 -122
  8. data/README.md +53 -14
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/ci/pipelines/pull-request.yml.erb +29 -32
  12. data/ci/pipelines/release.yml.erb +17 -41
  13. data/ci/scripts/run-tests.sh +20 -4
  14. data/ci/tasks/rubocop.yml +3 -3
  15. data/ci/tasks/update-changelog.yml +2 -2
  16. data/dlf +6 -1
  17. data/lib/license_finder/cli/base.rb +2 -0
  18. data/lib/license_finder/cli/licenses.rb +8 -3
  19. data/lib/license_finder/cli/main.rb +3 -1
  20. data/lib/license_finder/configuration.rb +8 -0
  21. data/lib/license_finder/core.rb +4 -2
  22. data/lib/license_finder/decision_applier.rb +1 -1
  23. data/lib/license_finder/decisions.rb +24 -6
  24. data/lib/license_finder/license/definitions.rb +129 -19
  25. data/lib/license_finder/license/templates/AGPL3.txt +661 -0
  26. data/lib/license_finder/license/templates/Apache2.txt +0 -2
  27. data/lib/license_finder/license/templates/Artistic.txt +128 -0
  28. data/lib/license_finder/license/templates/CC01_alt.txt +31 -0
  29. data/lib/license_finder/license/templates/CDDL1_1.txt +123 -0
  30. data/lib/license_finder/license/templates/CPL1.txt +217 -0
  31. data/lib/license_finder/license/templates/EPL2.txt +80 -0
  32. data/lib/license_finder/license/templates/Unlicense.txt +24 -0
  33. data/lib/license_finder/license/text.rb +4 -0
  34. data/lib/license_finder/license.rb +1 -1
  35. data/lib/license_finder/manual_licenses.rb +79 -0
  36. data/lib/license_finder/package.rb +1 -0
  37. data/lib/license_finder/package_manager.rb +2 -1
  38. data/lib/license_finder/package_managers/cargo.rb +1 -1
  39. data/lib/license_finder/package_managers/conan.rb +50 -8
  40. data/lib/license_finder/package_managers/dep.rb +43 -41
  41. data/lib/license_finder/package_managers/dotnet.rb +5 -2
  42. data/lib/license_finder/package_managers/go_dep.rb +1 -1
  43. data/lib/license_finder/package_managers/go_workspace.rb +3 -2
  44. data/lib/license_finder/package_managers/maven.rb +18 -10
  45. data/lib/license_finder/package_managers/npm.rb +14 -1
  46. data/lib/license_finder/package_managers/nuget.rb +5 -0
  47. data/lib/license_finder/package_managers/pip.rb +1 -1
  48. data/lib/license_finder/package_managers/pnpm.rb +126 -0
  49. data/lib/license_finder/package_managers/yarn.rb +69 -20
  50. data/lib/license_finder/package_utils/conan_info_parser.rb +2 -2
  51. data/lib/license_finder/package_utils/conan_info_parser_v2.rb +82 -0
  52. data/lib/license_finder/package_utils/license_files.rb +12 -2
  53. data/lib/license_finder/package_utils/licensing.rb +2 -1
  54. data/lib/license_finder/package_utils/maven_dependency_finder.rb +43 -1
  55. data/lib/license_finder/package_utils/notice_files.rb +14 -3
  56. data/lib/license_finder/package_utils/possible_license_file.rb +8 -2
  57. data/lib/license_finder/package_utils/pypi.rb +3 -1
  58. data/lib/license_finder/packages/maven_package.rb +13 -1
  59. data/lib/license_finder/packages/npm_package.rb +56 -9
  60. data/lib/license_finder/packages/pnpm_package.rb +13 -0
  61. data/lib/license_finder/printer.rb +2 -2
  62. data/lib/license_finder/reports/csv_report.rb +10 -1
  63. data/lib/license_finder/scanner.rb +3 -3
  64. data/license_finder.gemspec +12 -11
  65. metadata +54 -28
@@ -2,7 +2,12 @@
2
2
 
3
3
  module LicenseFinder
4
4
  class Yarn < PackageManager
5
- SHELL_COMMAND = 'yarn licenses list --json'
5
+ def initialize(options = {})
6
+ super
7
+ @yarn_options = options[:yarn_options]
8
+ end
9
+
10
+ SHELL_COMMAND = 'yarn licenses list --recursive --json'
6
11
 
7
12
  def possible_package_paths
8
13
  [project_path.join('yarn.lock')]
@@ -14,31 +19,20 @@ module LicenseFinder
14
19
  if yarn_version == 1
15
20
  cmd += ' --no-progress'
16
21
  cmd += " --cwd #{project_path}" unless project_path.nil?
22
+ cmd += " #{@yarn_options}" unless @yarn_options.nil?
17
23
  end
18
24
 
19
- stdout, stderr, status = Cmd.run(cmd)
25
+ stdout, stderr, status = Dir.chdir(project_path) { Cmd.run(cmd) }
20
26
  raise "Command '#{cmd}' failed to execute: #{stderr}" unless status.success?
21
27
 
22
- packages = []
23
- incompatible_packages = []
24
-
25
28
  json_strings = stdout.encode('ASCII', invalid: :replace, undef: :replace, replace: '?').split("\n")
26
29
  json_objects = json_strings.map { |json_object| JSON.parse(json_object) }
27
30
 
28
- if json_objects.last['type'] == 'table'
29
- license_json = json_objects.pop['data']
30
- packages = packages_from_json(license_json)
31
- end
32
-
33
- json_objects.each do |json_object|
34
- match = /(?<name>[\w,\-]+)@(?<version>(\d+\.?)+)/ =~ json_object['data'].to_s
35
- if match
36
- package = YarnPackage.new(name, version, spec_licenses: ['unknown'])
37
- incompatible_packages.push(package)
38
- end
31
+ if yarn_version == 1
32
+ get_yarn1_packages(json_objects)
33
+ else
34
+ get_yarn_packages(json_objects)
39
35
  end
40
-
41
- packages + incompatible_packages.uniq
42
36
  end
43
37
 
44
38
  def prepare
@@ -86,7 +80,7 @@ module LicenseFinder
86
80
 
87
81
  def yarn_version
88
82
  Dir.chdir(project_path) do
89
- version_string, stderr_str, status = Cmd.run('yarn -v')
83
+ version_string, stderr_str, status = Dir.chdir(project_path) { Cmd.run('yarn -v') }
90
84
  raise "Command 'yarn -v' failed to execute: #{stderr_str}" unless status.success?
91
85
 
92
86
  version = version_string.split('.').map(&:to_i)
@@ -94,6 +88,61 @@ module LicenseFinder
94
88
  end
95
89
  end
96
90
 
91
+ def get_yarn_packages(json_objects)
92
+ packages = []
93
+ incompatible_packages = []
94
+ json_objects.each do |json_object|
95
+ license = json_object['value']
96
+ body = json_object['children']
97
+
98
+ body.each do |package_name, vendor_info|
99
+ valid_match = %r{(?<name>@?[\w/.-]+)@(?<manager>\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s
100
+ valid_match = %r{(?<name>@?[\w/.-]+)@virtual:.+#(\D*):\D*(?<version>(\d+\.?)+)} =~ package_name.to_s if manager.eql?('virtual')
101
+
102
+ if valid_match
103
+ homepage = vendor_info['children']['vendorUrl']
104
+ author = vendor_info['children']['vendorName']
105
+ package = YarnPackage.new(
106
+ name,
107
+ version,
108
+ spec_licenses: [license],
109
+ homepage: homepage,
110
+ authors: author,
111
+ install_path: project_path.join(modules_folder, name)
112
+ )
113
+ packages << package
114
+ end
115
+
116
+ incompatible_match = %r{(?<name>@?[\w/.-]+)@[a-z]*:(?<version>(\.))} =~ package_name.to_s
117
+ if incompatible_match
118
+ package = YarnPackage.new(name, version, spec_licenses: [license])
119
+ incompatible_packages.push(package)
120
+ end
121
+ end
122
+ end
123
+
124
+ packages + incompatible_packages.uniq
125
+ end
126
+
127
+ def get_yarn1_packages(json_objects)
128
+ packages = []
129
+ if json_objects.last['type'] == 'table'
130
+ license_json = json_objects.pop['data']
131
+ packages = packages_from_json(license_json)
132
+ end
133
+
134
+ incompatible_packages = []
135
+ json_objects.each do |json_object|
136
+ match = %r{(?<name>@?[\w/.-]+)@(?<version>(\d+\.?)+)} =~ json_object['data'].to_s
137
+ if match
138
+ package = YarnPackage.new(name, version, spec_licenses: ['unknown'])
139
+ incompatible_packages.push(package)
140
+ end
141
+ end
142
+
143
+ packages + incompatible_packages.uniq
144
+ end
145
+
97
146
  def packages_from_json(json_data)
98
147
  body = json_data['body']
99
148
  head = json_data['head']
@@ -119,7 +168,7 @@ module LicenseFinder
119
168
  def modules_folder
120
169
  return @modules_folder if @modules_folder
121
170
 
122
- stdout, _stderr, status = Cmd.run('yarn config get modules-folder')
171
+ stdout, _stderr, status = Dir.chdir(project_path) { Cmd.run('yarn config get modules-folder') }
123
172
  @modules_folder = 'node_modules' if !status.success? || stdout.strip == 'undefined'
124
173
  @modules_folder ||= stdout.strip
125
174
  end
@@ -31,9 +31,9 @@ module LicenseFinder
31
31
  def parse_key_val(line)
32
32
  key, val = key_val(line)
33
33
  if val
34
- @current_project[key] = val
34
+ @current_project[key.downcase] = val
35
35
  elsif line.start_with?(' ')
36
- @current_key = key
36
+ @current_key = key.downcase
37
37
  @current_vals = []
38
38
  @state = :val_list
39
39
  else
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LicenseFinder
4
+ class ConanInfoParserV2
5
+ def parse(info)
6
+ @lines = info.lines.map(&:chomp)
7
+ @state = :project_level # state of the state machine
8
+ @projects = [] # list of projects
9
+ @current_project = nil # current project being populated in the SM
10
+ @current_vals = [] # current val list being populate in the SM
11
+ @current_key = nil # current key to be associated with the current val
12
+
13
+ line = @lines.shift
14
+ line = @lines.shift while line != '======== Basic graph information ========'
15
+
16
+ while (line = @lines.shift)
17
+ next if line == ''
18
+
19
+ case @state
20
+ when :project_level
21
+ @current_project = {}
22
+ name, _id = line.strip.split('#')
23
+ @current_project['name'] = name
24
+ @state = :key_val
25
+ when :key_val
26
+ parse_key_val(line)
27
+ when :val_list
28
+ parse_val_list(line)
29
+ end
30
+ end
31
+ wrap_up
32
+ end
33
+
34
+ private
35
+
36
+ def parse_key_val(line)
37
+ key, val = key_val(line)
38
+ if val
39
+ @current_project[key.downcase] = val
40
+ elsif line.start_with?(' ')
41
+ @current_key = key.downcase
42
+ @current_vals = []
43
+ @state = :val_list
44
+ else
45
+ change_to_new_project_state line
46
+ end
47
+ end
48
+
49
+ def parse_val_list(line)
50
+ if val_list_level(line)
51
+ @current_vals << line.strip
52
+ else
53
+ @current_project[@current_key] = @current_vals
54
+ if line.start_with?(' ')
55
+ @state = :key_val
56
+ @lines.unshift(line)
57
+ else
58
+ change_to_new_project_state line
59
+ end
60
+ end
61
+ end
62
+
63
+ def wrap_up
64
+ @current_project[@current_key] = @current_vals if @current_vals.count && @current_key
65
+ @projects << @current_project
66
+ end
67
+
68
+ def val_list_level(line)
69
+ line.start_with?(' ')
70
+ end
71
+
72
+ def change_to_new_project_state(line)
73
+ @state = :project_level
74
+ @projects << @current_project
75
+ @lines.unshift(line)
76
+ end
77
+
78
+ def key_val(info)
79
+ info.split(':', 2).map(&:strip!)
80
+ end
81
+ end
82
+ end
@@ -27,7 +27,7 @@ module LicenseFinder
27
27
 
28
28
  def paths_of_candidate_files
29
29
  candidate_files_and_dirs
30
- .flat_map { |path| path.directory? ? path.children : path }
30
+ .flat_map { |path| !path.is_a?(Zip::Entry) && path.directory? ? path.children : path }
31
31
  .reject(&:directory?)
32
32
  .uniq
33
33
  end
@@ -35,7 +35,17 @@ module LicenseFinder
35
35
  def candidate_files_and_dirs
36
36
  return [] if install_path.nil?
37
37
 
38
- Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
38
+ if !install_path.extname.casecmp('.jar').zero?
39
+ Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
40
+ else
41
+ candidates_from_zip
42
+ end
43
+ end
44
+
45
+ def candidates_from_zip
46
+ Zip::File.open(install_path.to_s) do |zip_file|
47
+ zip_file.glob(CANDIDATE_PATH_WILDCARD, File::FNM_EXTGLOB)
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -11,7 +11,8 @@ module LicenseFinder
11
11
  if activations_from_decisions.any? then activations_from_decisions
12
12
  elsif activations_from_spec.any? then activations_from_spec
13
13
  elsif activations_from_files.any? then activations_from_files
14
- else [default_activation]
14
+ else
15
+ [default_activation]
15
16
  end
16
17
  end
17
18
 
@@ -2,14 +2,56 @@
2
2
 
3
3
  module LicenseFinder
4
4
  class MavenDependencyFinder
5
- def initialize(project_path)
5
+ def initialize(project_path, m2_path)
6
6
  @project_path = project_path
7
+ @m2_path = m2_path
7
8
  end
8
9
 
9
10
  def dependencies
11
+ options = {
12
+ 'GroupTags' => { 'licenses' => 'license', 'dependencies' => 'dependency' },
13
+ 'ForceArray' => %w[license dependency]
14
+ }
15
+
10
16
  Pathname
11
17
  .glob(@project_path.join('**', 'target', 'generated-resources', 'licenses.xml'))
12
18
  .map(&:read)
19
+ .flat_map { |xml| XmlSimple.xml_in(xml, options)['dependencies'] }
20
+ .reject(&:empty?)
21
+ .each { |dep| add_info_from_m2(dep) }
22
+ end
23
+
24
+ # Add the name of the JAR file to allow later retrieval of license and notice files,
25
+ # and add the name, description and URL from the POM XML file.
26
+ def add_info_from_m2(dep)
27
+ m2_artifact_dir = @m2_path
28
+ .join(dep['groupId'].tr('.', '/'))
29
+ .join(dep['artifactId'])
30
+ .join(dep['version'])
31
+ artifact_basename = "#{dep['artifactId']}-#{dep['version']}"
32
+
33
+ # Basic support for Maven classifiers. So far, only "jakarta" is supported. Unfortunately,
34
+ # we do not have access to the "classifier" field here (licenses.xml does not have it).
35
+ jar_file = m2_artifact_dir.join("#{artifact_basename}.jar")
36
+ jar_file = m2_artifact_dir.join("#{artifact_basename}-jakarta.jar") unless File.exist?(jar_file)
37
+
38
+ dep.store('jarFile', jar_file)
39
+
40
+ add_info_from_pom(m2_artifact_dir.join("#{artifact_basename}.pom"), dep)
41
+ end
42
+
43
+ # Extract name, description and URL from pom.xml
44
+ def add_info_from_pom(pom_file, dep)
45
+ pom = XmlSimple.xml_in(pom_file.read, { 'ForceArray' => false })
46
+
47
+ name = pom['name']
48
+ dep.store('summary', name) unless name.nil?
49
+
50
+ description = pom['description']
51
+ dep.store('description', description) unless description.nil?
52
+
53
+ url = pom['url']
54
+ dep.store('homepage', url) unless url.nil?
13
55
  end
14
56
  end
15
57
  end
@@ -5,7 +5,8 @@ require 'license_finder/package_utils/possible_license_file'
5
5
  module LicenseFinder
6
6
  class NoticeFiles
7
7
  CANDIDATE_FILE_NAMES = %w[NOTICE Notice].freeze
8
- CANDIDATE_PATH_WILDCARD = "*{#{CANDIDATE_FILE_NAMES.join(',')}}*"
8
+ CANDIDATE_PATH_WILDCARD_STRICT = "{#{CANDIDATE_FILE_NAMES.join(',')}}*"
9
+ CANDIDATE_PATH_WILDCARD = "*#{CANDIDATE_PATH_WILDCARD_STRICT}"
9
10
 
10
11
  def self.find(install_path, options = {})
11
12
  new(install_path).find(options)
@@ -26,7 +27,7 @@ module LicenseFinder
26
27
 
27
28
  def paths_of_candidate_files
28
29
  candidate_files_and_dirs
29
- .flat_map { |path| path.directory? ? path.children : path }
30
+ .flat_map { |path| !path.is_a?(Zip::Entry) && path.directory? ? path.children : path }
30
31
  .reject(&:directory?)
31
32
  .uniq
32
33
  end
@@ -34,7 +35,17 @@ module LicenseFinder
34
35
  def candidate_files_and_dirs
35
36
  return [] if install_path.nil?
36
37
 
37
- Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
38
+ if !install_path.extname.casecmp('.jar').zero?
39
+ Pathname.glob(install_path.join('**', CANDIDATE_PATH_WILDCARD))
40
+ else
41
+ candidates_from_zip
42
+ end
43
+ end
44
+
45
+ def candidates_from_zip
46
+ Zip::File.open(install_path.to_s) do |zip_file|
47
+ zip_file.glob("*/#{CANDIDATE_PATH_WILDCARD_STRICT}", File::FNM_EXTGLOB)
48
+ end
38
49
  end
39
50
  end
40
51
  end
@@ -3,7 +3,11 @@
3
3
  module LicenseFinder
4
4
  class PossibleLicenseFile
5
5
  def initialize(path, options = {})
6
- @path = Pathname(path)
6
+ if !path.is_a?(Zip::Entry)
7
+ @path = Pathname(path)
8
+ else
9
+ @zip_entry = path
10
+ end
7
11
  @logger = options[:logger]
8
12
  end
9
13
 
@@ -16,7 +20,9 @@ module LicenseFinder
16
20
  end
17
21
 
18
22
  def text
19
- if @path.exist?
23
+ if @zip_entry
24
+ @zip_entry.get_input_stream.read
25
+ elsif @path.exist?
20
26
  @text ||= (@path.respond_to?(:binread) ? @path.binread : @path.read)
21
27
  else
22
28
  @logger.info('ERROR', "#{@path} does not exists", color: :red)
@@ -25,7 +25,9 @@ module LicenseFinder
25
25
  def definition(name, version)
26
26
  response = request("https://pypi.org/pypi/#{name}/#{version}/json")
27
27
  response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body).fetch('info', {}) : {}
28
- rescue *CONNECTION_ERRORS
28
+ rescue *CONNECTION_ERRORS => e
29
+ raise e, "Unable to read package from pypi.org #{name} #{version}: #{e}" unless @prepare_no_fail
30
+
29
31
  {}
30
32
  end
31
33
 
@@ -5,13 +5,17 @@ module LicenseFinder
5
5
  def initialize(spec, options = {})
6
6
  name = spec['artifactId']
7
7
  name = "#{spec['groupId']}:#{name}" if options[:include_groups]
8
+ @jar_file = spec['jarFile']
8
9
 
9
10
  super(
10
11
  name,
11
12
  spec['version'],
12
13
  options.merge(
13
14
  spec_licenses: Array(spec['licenses']).map { |l| l['name'] },
14
- groups: Array(spec['groupId'])
15
+ groups: Array(spec['groupId']),
16
+ summary: spec['summary'],
17
+ description: spec['description'],
18
+ homepage: spec['homepage']
15
19
  )
16
20
  )
17
21
  end
@@ -23,5 +27,13 @@ module LicenseFinder
23
27
  def package_url
24
28
  "https://search.maven.org/artifact/#{CGI.escape(groups.first)}/#{CGI.escape(name.split(':').last)}/#{CGI.escape(version)}/jar"
25
29
  end
30
+
31
+ def license_files
32
+ LicenseFiles.find(@jar_file, logger: logger)
33
+ end
34
+
35
+ def notice_files
36
+ NoticeFiles.find(@jar_file, logger: logger)
37
+ end
26
38
  end
27
39
  end
@@ -23,12 +23,12 @@ module LicenseFinder
23
23
  def flattened_dependencies(npm_json, existing_packages = {})
24
24
  identifier = Identifier.from_hash npm_json
25
25
  if existing_packages[identifier].nil?
26
- existing_packages[identifier] = NpmPackage.new(npm_json) if identifier
26
+ existing_packages[identifier] = package_for_dependency(npm_json) if identifier
27
27
  npm_json.fetch('dependencies', {}).values.map do |d|
28
28
  flattened_dependencies(d, existing_packages)
29
29
  end
30
30
  else
31
- duplicate_package = NpmPackage.new(npm_json)
31
+ duplicate_package = package_for_dependency(npm_json)
32
32
  unless existing_packages[identifier].dependencies.include?(duplicate_package.dependencies)
33
33
  existing_packages[identifier].dependencies |= duplicate_package.dependencies
34
34
  npm_json.fetch('dependencies', {}).values.map do |d|
@@ -39,6 +39,23 @@ module LicenseFinder
39
39
  existing_packages
40
40
  end
41
41
 
42
+ # Read the dependency's package.json file in order to get details like the license, authors,
43
+ # and so on. In NPM versions < 7, this information was included in the output of `npm list`.
44
+ # In later versions, it no longer is, and has to be read from the package.json file instead.
45
+ def package_for_dependency(npm_json)
46
+ package_path = npm_json['path']
47
+ package_json_path = Pathname.new(package_path).join('package.json') unless package_path.nil?
48
+
49
+ if package_json_path.nil? || !package_json_path.exist?
50
+ # Ancient NPM versions did not have the "path" field. Resort to the old way of gathering
51
+ # the details, expecting them to be contained in the output of `npm list`.
52
+ NpmPackage.new(npm_json)
53
+ else
54
+ package_json = JSON.parse(package_json_path.read, max_nesting: false)
55
+ NpmPackage.new(npm_json, package_json)
56
+ end
57
+ end
58
+
42
59
  def populate_groups(package_json)
43
60
  package_json.groups.each do |group|
44
61
  group.package_names.each do |package_name|
@@ -64,19 +81,49 @@ module LicenseFinder
64
81
  end
65
82
  end
66
83
 
67
- def initialize(npm_json)
68
- @json = npm_json
84
+ def initialize(npm_json, package_json = npm_json)
85
+ @npm_json = npm_json
86
+ @json = package_json
69
87
  @identifier = Identifier.from_hash(npm_json)
70
88
  @dependencies = deps_from_json
71
89
  super(@identifier.name,
72
90
  @identifier.version,
73
- description: npm_json['description'],
74
- homepage: npm_json['homepage'],
75
- spec_licenses: Package.license_names_from_standard_spec(npm_json),
91
+ description: package_json['description'],
92
+ homepage: package_json['homepage'],
93
+ authors: author_names,
94
+ spec_licenses: Package.license_names_from_standard_spec(package_json),
76
95
  install_path: npm_json['path'],
77
96
  children: @dependencies.map(&:name))
78
97
  end
79
98
 
99
+ def author_names
100
+ names = []
101
+ if @json['author'].is_a?(Array)
102
+ # "author":["foo","bar"] isn't valid according to the NPM package.json schema, but can be found in the wild.
103
+ names += @json['author'].map { |a| author_name(a) }
104
+ else
105
+ names << author_name(@json['author']) unless @json['author'].nil?
106
+ end
107
+ names += @json['contributors'].map { |c| author_name(c) } if @json['contributors'].is_a?(Array)
108
+ names.compact.join(', ')
109
+ rescue TypeError
110
+ puts "Warning: Invalid author and/or contributors metadata found in package.json for #{@identifier}"
111
+ nil
112
+ end
113
+
114
+ def author_name(author)
115
+ if author.instance_of?(String)
116
+ author_name_from_combined(author)
117
+ else
118
+ author['name']
119
+ end
120
+ end
121
+
122
+ def author_name_from_combined(author)
123
+ matches = author.match /^(.*?)\s*(<.*?>)?\s*(\(.*?\))?\s*$/
124
+ matches[1]
125
+ end
126
+
80
127
  def ==(other)
81
128
  other.is_a?(NpmPackage) && @identifier == other.identifier
82
129
  end
@@ -96,7 +143,7 @@ module LicenseFinder
96
143
  private
97
144
 
98
145
  def deps_from_json
99
- @json.fetch('dependencies', {}).values.map { |dep| Identifier.from_hash(dep) }.compact
146
+ @npm_json.fetch('dependencies', {}).values.map { |dep| Identifier.from_hash(dep) }.compact
100
147
  end
101
148
 
102
149
  class Identifier
@@ -110,7 +157,7 @@ module LicenseFinder
110
157
  def self.from_hash(hash)
111
158
  name = hash['name']
112
159
  version = hash['version']
113
- return nil if name.nil? || version.nil?
160
+ return nil if name.nil? || name.empty? || version.nil? || version.empty?
114
161
 
115
162
  Identifier.new(name, version)
116
163
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LicenseFinder
4
+ class PNPMPackage < Package
5
+ def package_manager
6
+ 'PNPM'
7
+ end
8
+
9
+ def package_url
10
+ "https://www.npmjs.com/package/#{CGI.escape(name)}/v/#{CGI.escape(version)}"
11
+ end
12
+ end
13
+ end
@@ -4,7 +4,7 @@ module LicenseFinder
4
4
  class Printer
5
5
  attr_reader :padding
6
6
 
7
- def initialize #:nodoc:
7
+ def initialize # :nodoc:
8
8
  @base = nil
9
9
  @mute = false
10
10
  @padding = 0
@@ -24,7 +24,7 @@ module LicenseFinder
24
24
  spaces + set_color(message.to_s, *color)
25
25
  end
26
26
 
27
- def set_color(string, *) #:nodoc:
27
+ def set_color(string, *) # :nodoc:
28
28
  string
29
29
  end
30
30
 
@@ -4,7 +4,7 @@ module LicenseFinder
4
4
  class CsvReport < Report
5
5
  COMMA_SEP = ','.freeze
6
6
  NEWLINE_SEP = '\@NL'.freeze
7
- AVAILABLE_COLUMNS = %w[name version authors licenses license_links approved summary description homepage install_path package_manager groups texts notice].freeze
7
+ AVAILABLE_COLUMNS = %w[name version authors licenses license_links approved summary description homepage install_path package_manager groups texts notice approved_by approved_reason].freeze
8
8
  MISSING_DEPENDENCY_TEXT = 'This package is not installed. Please install to determine licenses.'.freeze
9
9
 
10
10
  def initialize(dependencies, options)
@@ -95,5 +95,14 @@ module LicenseFinder
95
95
  dep.groups.join(self.class::COMMA_SEP)
96
96
  end
97
97
  end
98
+
99
+ def format_approved_by(dep)
100
+ dep.approved_manually? ? dep.manual_approval.who : ''
101
+ end
102
+
103
+ def format_approved_reason(dep)
104
+ dep.approved_manually? ? dep.manual_approval.why : ''
105
+ end
106
+
98
107
  end
99
108
  end
@@ -3,7 +3,7 @@
3
3
  module LicenseFinder
4
4
  class Scanner
5
5
  PACKAGE_MANAGERS = [
6
- GoModules, GoDep, GoWorkspace, Go15VendorExperiment, Glide, Gvt, Govendor, Trash, Dep, Bundler, NPM, Pip,
6
+ GoModules, GoDep, GoWorkspace, Go15VendorExperiment, Glide, Gvt, Govendor, Trash, Bundler, NPM, PNPM, Pip,
7
7
  Yarn, Bower, Maven, Gradle, CocoaPods, Rebar, Erlangmk, Nuget, Carthage, Mix, Conan, Sbt, Cargo, Dotnet, Composer, Pipenv,
8
8
  Conda, Spm, Pub
9
9
  ].freeze
@@ -50,10 +50,10 @@ module LicenseFinder
50
50
  active = pm_class.new(@config).active?
51
51
 
52
52
  if active
53
- @logger.info pm_class, 'is active', color: :green
53
+ @logger.info pm_class, "is active for '#{@project_path}'", color: :green
54
54
  active_pm_classes << pm_class
55
55
  else
56
- @logger.debug pm_class, 'is not active', color: :red
56
+ @logger.debug pm_class, "is not active for '#{@project_path}'", color: :red
57
57
  end
58
58
  end
59
59
 
@@ -3,7 +3,7 @@
3
3
  version = File.read(File.expand_path('VERSION', __dir__)).strip
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.required_ruby_version = '>= 2.4.0'
6
+ s.required_ruby_version = '>= 2.6.0'
7
7
  s.name = 'license_finder'
8
8
  s.version = version
9
9
 
@@ -44,30 +44,31 @@ Gem::Specification.new do |s|
44
44
  s.license = 'MIT'
45
45
 
46
46
  s.add_dependency 'bundler'
47
+ s.add_dependency 'csv', '~> 3.2'
47
48
  s.add_dependency 'rubyzip', '>=1', '<3'
48
49
  s.add_dependency 'thor', '~> 1.2'
49
50
  s.add_dependency 'tomlrb', '>= 1.3', '< 2.1'
50
51
  s.add_dependency 'with_env', '1.1.0'
51
52
  s.add_dependency 'xml-simple', '~> 1.1.9'
52
53
 
53
- s.add_development_dependency 'addressable', '2.8.0'
54
- s.add_development_dependency 'capybara', '~> 3.32.2'
54
+ s.add_development_dependency 'addressable', '2.8.6'
55
+ s.add_development_dependency 'capybara', '~> 3.39.2'
55
56
  s.add_development_dependency 'cocoapods', '>= 1.0.0' if RUBY_PLATFORM.match?(/darwin/)
56
57
  s.add_development_dependency 'e2mmap', '~> 0.1.0'
57
- s.add_development_dependency 'fakefs', '~> 1.4.1'
58
- s.add_development_dependency 'matrix', '~> 0.1.0'
59
- s.add_development_dependency 'mime-types', '3.4.1'
58
+ s.add_development_dependency 'fakefs', '~> 2.5.0'
59
+ s.add_development_dependency 'matrix', '~> 0.4.2'
60
+ s.add_development_dependency 'mime-types', '3.5.2'
60
61
  s.add_development_dependency 'pry', '~> 0.14.1'
61
- s.add_development_dependency 'rake', '~> 13.0.6'
62
+ s.add_development_dependency 'rake', '~> 13.1.0'
62
63
  s.add_development_dependency 'rspec', '~> 3'
63
64
  s.add_development_dependency 'rspec-its', '~> 1.3.0'
64
- s.add_development_dependency 'rubocop', '~> 1.12.1'
65
- s.add_development_dependency 'rubocop-performance', '~> 1.10.2'
65
+ s.add_development_dependency 'rubocop', '~> 1.60.2'
66
+ s.add_development_dependency 'rubocop-performance', '~> 1.20.2'
66
67
  s.add_development_dependency 'webmock', '~> 3.14'
67
68
 
68
69
  s.add_development_dependency 'nokogiri', '~>1.10'
69
- s.add_development_dependency 'rack', '~> 2.2.3'
70
- s.add_development_dependency 'rack-test', '~> 1.1.0', '> 0.7'
70
+ s.add_development_dependency 'rack', '~> 3.0.0'
71
+ s.add_development_dependency 'rack-test', '> 0.7', '~> 2.1.0'
71
72
 
72
73
  s.files = `git ls-files`.split("\n").reject { |f| f.start_with?('spec', 'features') }
73
74
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }