license_finder 7.0.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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) }