licensed 2.15.2 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +55 -11
  3. data/CHANGELOG.md +56 -1
  4. data/README.md +38 -81
  5. data/docs/adding_a_new_source.md +11 -8
  6. data/docs/commands/README.md +59 -0
  7. data/docs/commands/cache.md +35 -0
  8. data/docs/commands/env.md +10 -0
  9. data/docs/commands/list.md +23 -0
  10. data/docs/commands/migrate.md +10 -0
  11. data/docs/commands/notices.md +12 -0
  12. data/docs/commands/status.md +73 -0
  13. data/docs/commands/version.md +3 -0
  14. data/docs/configuration.md +9 -161
  15. data/docs/configuration/README.md +11 -0
  16. data/docs/configuration/allowed_licenses.md +17 -0
  17. data/docs/configuration/application_name.md +63 -0
  18. data/docs/configuration/application_source.md +64 -0
  19. data/docs/configuration/configuration_root.md +27 -0
  20. data/docs/configuration/configuring_multiple_apps.md +58 -0
  21. data/docs/configuration/dependency_source_enumerators.md +28 -0
  22. data/docs/configuration/ignoring_dependencies.md +19 -0
  23. data/docs/configuration/metadata_cache.md +106 -0
  24. data/docs/configuration/reviewing_dependencies.md +18 -0
  25. data/docs/{migrating_to_newer_versions.md → migrations/v2.md} +1 -1
  26. data/docs/migrations/v3.md +109 -0
  27. data/docs/sources/bundler.md +1 -11
  28. data/docs/sources/swift.md +4 -0
  29. data/lib/licensed.rb +1 -0
  30. data/lib/licensed/cli.rb +6 -3
  31. data/lib/licensed/commands/cache.rb +19 -20
  32. data/lib/licensed/commands/command.rb +104 -72
  33. data/lib/licensed/commands/environment.rb +12 -11
  34. data/lib/licensed/commands/list.rb +0 -19
  35. data/lib/licensed/commands/notices.rb +0 -19
  36. data/lib/licensed/commands/status.rb +13 -15
  37. data/lib/licensed/configuration.rb +105 -12
  38. data/lib/licensed/report.rb +44 -0
  39. data/lib/licensed/reporters/cache_reporter.rb +48 -64
  40. data/lib/licensed/reporters/json_reporter.rb +19 -21
  41. data/lib/licensed/reporters/list_reporter.rb +45 -58
  42. data/lib/licensed/reporters/notices_reporter.rb +33 -46
  43. data/lib/licensed/reporters/reporter.rb +37 -104
  44. data/lib/licensed/reporters/status_reporter.rb +58 -56
  45. data/lib/licensed/reporters/yaml_reporter.rb +19 -21
  46. data/lib/licensed/sources.rb +1 -0
  47. data/lib/licensed/sources/bundler.rb +36 -217
  48. data/lib/licensed/sources/bundler/missing_specification.rb +54 -0
  49. data/lib/licensed/sources/go.rb +1 -1
  50. data/lib/licensed/sources/gradle.rb +2 -2
  51. data/lib/licensed/sources/npm.rb +4 -3
  52. data/lib/licensed/sources/nuget.rb +57 -27
  53. data/lib/licensed/sources/swift.rb +69 -0
  54. data/lib/licensed/version.rb +1 -1
  55. data/script/source-setup/go +1 -1
  56. data/script/source-setup/swift +22 -0
  57. metadata +27 -4
  58. data/docs/commands.md +0 -95
@@ -15,22 +15,17 @@ module Licensed
15
15
 
16
16
  protected
17
17
 
18
- # Run the command for all enumerated dependencies found in a dependency source,
19
- # recording results in a report.
20
- # Enumerating dependencies in the source is skipped if a :sources option
21
- # is provided and the evaluated `source.class.type` is not in the :sources values
18
+ # Run the comand and set an error message to review the documentation
19
+ # when any errors have been reported
22
20
  #
23
- # app - The application configuration for the source
24
- # source - A dependency source enumerator
21
+ # report - A Licensed::Report object for this command
25
22
  #
26
- # Returns whether the command succeeded for the dependency source enumerator
27
- def run_source(app, source)
28
- super do |report|
29
- next if Array(options[:sources]).empty?
30
- next if options[:sources].include?(source.class.type)
31
-
32
- report.warnings << "skipped source"
33
- :skip
23
+ # Returns whether the command succeeded based on the call to super
24
+ def run_command(report)
25
+ super do |result|
26
+ next if result
27
+
28
+ report.errors << "Licensed found errors during source enumeration. Please see https://github.com/github/licensed/tree/master/docs/commands/status.md#status-errors-and-resolutions for possible resolutions."
34
29
  end
35
30
  end
36
31
 
@@ -47,11 +42,14 @@ module Licensed
47
42
  def evaluate_dependency(app, source, dependency, report)
48
43
  filename = app.cache_path.join(source.class.type, "#{dependency.name}.#{DependencyRecord::EXTENSION}")
49
44
  report["filename"] = filename
45
+ report["version"] = dependency.version
50
46
 
51
47
  cached_record = cached_record(filename)
52
48
  if cached_record.nil?
49
+ report["license"] = nil
53
50
  report.errors << "cached dependency record not found"
54
51
  else
52
+ report["license"] = cached_record["license"]
55
53
  report.errors << "cached dependency record out of date" if cached_record["version"] != dependency.version
56
54
  report.errors << "missing license text" if cached_record.licenses.empty?
57
55
  if cached_record["review_changed_license"]
@@ -61,7 +59,7 @@ module Licensed
61
59
  end
62
60
  end
63
61
 
64
- report.errors.empty?
62
+ report["allowed"] = report.errors.empty?
65
63
  end
66
64
 
67
65
  # Returns true if a cached record needs further review based on the
@@ -3,9 +3,14 @@ require "pathname"
3
3
 
4
4
  module Licensed
5
5
  class AppConfiguration < Hash
6
+ DIRECTORY_NAME_GENERATOR_KEY = "directory_name".freeze
7
+ RELATIVE_PATH_GENERATOR_KEY = "relative_path".freeze
8
+ DEFAULT_RELATIVE_PATH_NAME_SEPARATOR = "-".freeze
9
+ ALL_NAME_GENERATOR_KEYS = [DIRECTORY_NAME_GENERATOR_KEY, RELATIVE_PATH_GENERATOR_KEY].freeze
10
+
6
11
  DEFAULT_CACHE_PATH = ".licenses".freeze
7
12
 
8
- # Returns the root for a configuration in following order of precendence:
13
+ # Returns the root for a configuration in following order of precedence:
9
14
  # 1. explicitly configured "root" property
10
15
  # 2. a found git repository root
11
16
  # 3. the current directory
@@ -28,9 +33,9 @@ module Licensed
28
33
  self["ignored"] ||= {}
29
34
  self["allowed"] ||= []
30
35
  self["root"] = AppConfiguration.root_for(self)
31
- # defaults to the directory name of the source path if not set
32
- self["name"] ||= File.basename(self["source_path"])
33
- # setting the cache path might need a valid app name
36
+ self["name"] = generate_app_name
37
+ # setting the cache path might need a valid app name.
38
+ # this must come after setting self["name"]
34
39
  self["cache_path"] = detect_cache_path(options, inherited_options)
35
40
  end
36
41
 
@@ -105,8 +110,9 @@ module Licensed
105
110
 
106
111
  # Returns the cache path for the application based on:
107
112
  # 1. An explicitly set cache path for the application, if set
108
- # 2. An inherited root cache path joined with the app name
109
- # 3. The default cache path joined with the app name
113
+ # 2. An inherited shared cache path
114
+ # 3. An inherited cache path joined with the app name if not shared
115
+ # 4. The default cache path joined with the app name
110
116
  def detect_cache_path(options, inherited_options)
111
117
  return options["cache_path"] unless options["cache_path"].to_s.empty?
112
118
 
@@ -124,6 +130,65 @@ module Licensed
124
130
  raise Licensed::Configuration::LoadError,
125
131
  "App #{self["name"]} is missing required property #{property}"
126
132
  end
133
+
134
+ # Returns a name for the application as one of:
135
+ # 1. An explicitly configured app name, if set
136
+ # 2. A generated app name based on an configured "name" options hash
137
+ # 3. A default value - the source_path directory name
138
+ def generate_app_name
139
+ # use default_app_name if a name value is not set
140
+ return source_path_directory_app_name if self["name"].to_s.empty?
141
+ # keep the same name value unless a hash is given with naming options
142
+ return self["name"] unless self["name"].is_a?(Hash)
143
+
144
+ generator = self.dig("name", "generator")
145
+ case generator
146
+ when nil, DIRECTORY_NAME_GENERATOR_KEY
147
+ source_path_directory_app_name
148
+ when RELATIVE_PATH_GENERATOR_KEY
149
+ relative_path_app_name
150
+ else
151
+ raise Licensed::Configuration::LoadError,
152
+ "Invalid value configured for name.generator: #{generator}. Value must be one of #{ALL_NAME_GENERATOR_KEYS.join(",")}"
153
+ end
154
+ end
155
+
156
+ # Returns an app name from the directory name of the configured source path
157
+ def source_path_directory_app_name
158
+ File.basename(self["source_path"])
159
+ end
160
+
161
+ # Returns an app name from the relative path from the configured app root
162
+ # to the configured app source path.
163
+ def relative_path_app_name
164
+ source_path_parts = File.expand_path(self["source_path"]).split("/")
165
+ root_path_parts = File.expand_path(self["root"]).split("/")
166
+
167
+ # if the source path is equivalent to the root path,
168
+ # return the root directory name
169
+ return root_path_parts[-1] if source_path_parts == root_path_parts
170
+
171
+ if source_path_parts[0..root_path_parts.size-1] != root_path_parts
172
+ raise Licensed::Configuration::LoadError,
173
+ "source_path must be a descendent of the app root to generate an app name from the relative source_path"
174
+ end
175
+
176
+ name_parts = source_path_parts[root_path_parts.size..-1]
177
+
178
+ separator = self.dig("name", "separator") || DEFAULT_RELATIVE_PATH_NAME_SEPARATOR
179
+ depth = self.dig("name", "depth") || 0
180
+ if depth < 0
181
+ raise Licensed::Configuration::LoadError, "name.depth configuration value cannot be less than -1"
182
+ end
183
+
184
+ # offset the depth value by -1 to work as an offset from the end of the array
185
+ # 0 becomes -1, with a start index of (-1 - -1) = 0, or the full array
186
+ # 1 becomes 0, with a start index of (-1 - 0) = -1, or only the last element
187
+ # and so on...
188
+ depth = depth - 1
189
+ start_index = depth >= name_parts.length ? 0 : -1 - depth
190
+ name_parts[start_index..-1].join(separator)
191
+ end
127
192
  end
128
193
 
129
194
  class Configuration
@@ -151,6 +216,11 @@ module Licensed
151
216
  def initialize(options = {})
152
217
  apps = options.delete("apps") || []
153
218
  apps << default_options.merge(options) if apps.empty?
219
+
220
+ # apply a root setting to all app configurations so that it's available
221
+ # when expanding app source paths
222
+ apps.each { |app| app["root"] ||= options["root"] if options["root"] }
223
+
154
224
  apps = apps.flat_map { |app| self.class.expand_app_source_path(app) }
155
225
  @apps = apps.map { |app| AppConfiguration.new(app, options) }
156
226
  end
@@ -158,25 +228,48 @@ module Licensed
158
228
  private
159
229
 
160
230
  def self.expand_app_source_path(app_config)
161
- return app_config if app_config["source_path"].to_s.empty?
231
+ # map a source_path configuration value to an array of non-empty values
232
+ source_path_array = Array(app_config["source_path"])
233
+ .reject { |path| path.to_s.empty? }
234
+ .compact
235
+ app_root = AppConfiguration.root_for(app_config)
236
+ return app_config.merge("source_path" => app_root) if source_path_array.empty?
162
237
 
163
238
  # check if the source path maps to an existing directory
164
- source_path = File.expand_path(app_config["source_path"], AppConfiguration.root_for(app_config))
165
- return app_config if Dir.exist?(source_path)
239
+ if source_path_array.length == 1
240
+ source_path = File.expand_path(source_path_array[0], app_root)
241
+ return app_config.merge("source_path" => source_path) if Dir.exist?(source_path)
242
+ end
166
243
 
167
244
  # try to expand the source path for glob patterns
168
- expanded_source_paths = Dir.glob(source_path).select { |p| File.directory?(p) }
245
+ expanded_source_paths = source_path_array.reduce(Set.new) do |matched_paths, pattern|
246
+ current_matched_paths = if pattern.start_with?("!")
247
+ # if the pattern is an exclusion, remove all matching files
248
+ # from the result
249
+ matched_paths - Dir.glob(pattern[1..-1])
250
+ else
251
+ # if the pattern is an inclusion, add all matching files
252
+ # to the result
253
+ matched_paths + Dir.glob(pattern)
254
+ end
255
+
256
+ current_matched_paths.select { |p| File.directory?(p) }
257
+ end
258
+
169
259
  configs = expanded_source_paths.map { |path| app_config.merge("source_path" => path) }
170
260
 
171
261
  # if no directories are found for the source path, return the original config
172
- return app_config if configs.size == 0
262
+ if configs.size == 0
263
+ app_config["source_path"] = app_root if app_config["source_path"].is_a?(Array)
264
+ return app_config
265
+ end
173
266
 
174
267
  # update configured values for name and cache_path for uniqueness.
175
268
  # this is only needed when values are explicitly set, AppConfiguration
176
269
  # will handle configurations that don't have these explicitly set
177
270
  configs.each do |config|
178
271
  dir_name = File.basename(config["source_path"])
179
- config["name"] = "#{config["name"]}-#{dir_name}" if config["name"]
272
+ config["name"] = "#{config["name"]}-#{dir_name}" if config["name"].is_a?(String)
180
273
 
181
274
  # if a cache_path is set and is not marked as shared, append the app name
182
275
  # to the end of the cache path to make a unique cache path for the app
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Licensed
4
+ class Report < Hash
5
+ attr_reader :name
6
+ attr_reader :target
7
+ def initialize(name:, target:)
8
+ super()
9
+ @name = name
10
+ @target = target
11
+ end
12
+
13
+ def reports
14
+ @reports ||= []
15
+ end
16
+
17
+ def errors
18
+ @errors ||= []
19
+ end
20
+
21
+ def warnings
22
+ @warnings ||= []
23
+ end
24
+
25
+ def all_reports
26
+ result = []
27
+ result << self
28
+ result.push(*reports.flat_map(&:all_reports))
29
+ end
30
+
31
+ # Returns the data from the report as a hash
32
+ def to_h
33
+ # add name, errors and warnings if they have real data
34
+ output = {}
35
+ output["name"] = name unless name.to_s.empty?
36
+ output["errors"] = errors.dup if errors.any?
37
+ output["warnings"] = warnings.dup if warnings.any?
38
+
39
+ # merge the hash data from the report. command-specified data always
40
+ # overwrites local data
41
+ output.merge(super)
42
+ end
43
+ end
44
+ end
@@ -2,89 +2,73 @@
2
2
  module Licensed
3
3
  module Reporters
4
4
  class CacheReporter < Reporter
5
- # Reports on an application configuration in a cache command run
5
+ # Reports the start of caching records for an app
6
6
  #
7
7
  # app - An application configuration
8
- #
9
- # Returns the result of the yielded method
10
- # Note - must be called from inside the `report_run` scope
11
- def report_app(app)
12
- super do |report|
13
- shell.info "Caching dependency records for #{app["name"]}"
14
- yield report
15
- end
8
+ # report - A report containing information about the app evaluation
9
+ def begin_report_app(app, report)
10
+ shell.info "Caching dependency records for #{app["name"]}"
16
11
  end
17
12
 
18
- # Reports on a dependency source enumerator in a cache command run.
19
- # Shows the type and count of dependencies found by the source.
13
+ # Reports the start of caching records for a dependency source
20
14
  #
21
15
  # source - A dependency source enumerator
22
- #
23
- # Returns the result of the yielded method
24
- # Note - must be called from inside the `report_run` scope
25
- def report_source(source)
26
- super do |report|
27
- shell.info " #{source.class.type}"
28
- result = yield report
16
+ # report - A report containing information about the source evaluation
17
+ def begin_report_source(source, report)
18
+ shell.info " #{source.class.type}"
19
+ end
29
20
 
30
- warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
31
- if warning_reports.any?
32
- shell.newline
33
- shell.warn " * Warnings:"
34
- warning_reports.each do |r|
35
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
21
+ # Reports warnings and errors found while evaluating the dependency source
22
+ #
23
+ # source - A dependency source enumerator
24
+ # report - A report containing information about the source evaluation
25
+ def end_report_source(source, report)
26
+ warning_reports = report.all_reports.select { |r| r.warnings.any? }.to_a
27
+ if warning_reports.any?
28
+ shell.newline
29
+ shell.warn " * Warnings:"
30
+ warning_reports.each do |r|
31
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
36
32
 
37
- shell.warn " * #{r.name}"
38
- shell.warn " #{display_metadata}" unless display_metadata.empty?
39
- r.warnings.each do |warning|
40
- shell.warn " - #{warning}"
41
- end
42
- shell.newline
33
+ shell.warn " * #{r.name}"
34
+ shell.warn " #{display_metadata}" unless display_metadata.empty?
35
+ r.warnings.each do |warning|
36
+ shell.warn " - #{warning}"
43
37
  end
38
+ shell.newline
44
39
  end
40
+ end
45
41
 
46
- errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
47
- if errored_reports.any?
48
- shell.newline
49
- shell.error " * Errors:"
50
- errored_reports.each do |r|
51
- display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
42
+ errored_reports = report.all_reports.select { |r| r.errors.any? }.to_a
43
+ if errored_reports.any?
44
+ shell.newline
45
+ shell.error " * Errors:"
46
+ errored_reports.each do |r|
47
+ display_metadata = r.map { |k, v| "#{k}: #{v}" }.join(", ")
52
48
 
53
- shell.error " * #{r.name}"
54
- shell.error " #{display_metadata}" unless display_metadata.empty?
55
- r.errors.each do |error|
56
- shell.error " - #{error}"
57
- end
58
- shell.newline
49
+ shell.error " * #{r.name}"
50
+ shell.error " #{display_metadata}" unless display_metadata.empty?
51
+ r.errors.each do |error|
52
+ shell.error " - #{error}"
59
53
  end
60
- else
61
- shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
54
+ shell.newline
62
55
  end
63
-
64
- result
56
+ else
57
+ shell.confirm " * #{report.reports.size} #{source.class.type} dependencies"
65
58
  end
66
59
  end
67
60
 
68
- # Reports on a dependency in a cache command run.
69
- # Shows whether the dependency's record was cached or reused.
61
+ # Reports whether the dependency's record was cached or reused.
70
62
  #
71
63
  # dependency - An application dependency
72
- #
73
- # Returns the result of the yielded method
74
- # Note - must be called from inside the `report_run` scope
75
- def report_dependency(dependency)
76
- super do |report|
77
- result = yield report
78
-
79
- if report.errors.any?
80
- shell.error " Error #{dependency.name} (#{dependency.version})"
81
- elsif report["cached"]
82
- shell.info " Caching #{dependency.name} (#{dependency.version})"
83
- else
84
- shell.info " Using #{dependency.name} (#{dependency.version})"
85
- end
86
-
87
- result
64
+ # report - A report containing information about the dependency evaluation
65
+ def end_report_dependency(dependency, report)
66
+ if report.errors.any?
67
+ shell.error " Error #{dependency.name} (#{dependency.version})"
68
+ elsif report["cached"]
69
+ shell.info " Caching #{dependency.name} (#{dependency.version})"
70
+ else
71
+ shell.info " Using #{dependency.name} (#{dependency.version})"
88
72
  end
89
73
  end
90
74
  end
@@ -4,31 +4,29 @@ require "json"
4
4
  module Licensed
5
5
  module Reporters
6
6
  class JsonReporter < Reporter
7
- def report_run(command)
8
- super do |report|
9
- result = yield report
10
-
11
- report["apps"] = report.reports.map(&:to_h) if report.reports.any?
12
- shell.info JSON.pretty_generate(report.to_h)
13
-
14
- result
15
- end
7
+ # Report all information from the command run to the shell as a JSON object
8
+ #
9
+ # command - The command being run
10
+ # report - A report object containing information about the command run
11
+ def end_report_command(command, report)
12
+ report["apps"] = report.reports.map(&:to_h) if report.reports.any?
13
+ shell.info JSON.pretty_generate(report.to_h)
16
14
  end
17
15
 
18
- def report_app(app)
19
- super do |report|
20
- result = yield report
21
- report["sources"] = report.reports.map(&:to_h) if report.reports.any?
22
- result
23
- end
16
+ # Add source report information to the app report hash
17
+ #
18
+ # app - An application configuration
19
+ # report - A report object containing information about the app evaluation
20
+ def end_report_app(app, report)
21
+ report["sources"] = report.reports.map(&:to_h) if report.reports.any?
24
22
  end
25
23
 
26
- def report_source(source)
27
- super do |report|
28
- result = yield report
29
- report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
30
- result
31
- end
24
+ # Add dependency report information to the source report hash
25
+ #
26
+ # source - A dependency source enumerator
27
+ # report - A report object containing information about the source evaluation
28
+ def end_report_source(source, report)
29
+ report["dependencies"] = report.reports.map(&:to_h) if report.reports.any?
32
30
  end
33
31
  end
34
32
  end