licensee 8.9.2 → 9.0.0.beta.1

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +1 -1
  3. data/bin/licensee +42 -28
  4. data/lib/licensee.rb +14 -28
  5. data/lib/licensee/content_helper.rb +38 -12
  6. data/lib/licensee/license.rb +22 -9
  7. data/lib/licensee/matchers.rb +14 -0
  8. data/lib/licensee/matchers/cabal.rb +16 -0
  9. data/lib/licensee/matchers/{copyright_matcher.rb → copyright.rb} +1 -5
  10. data/lib/licensee/matchers/{cran_matcher.rb → cran.rb} +1 -1
  11. data/lib/licensee/matchers/{dice_matcher.rb → dice.rb} +1 -7
  12. data/lib/licensee/matchers/{dist_zilla_matcher.rb → dist_zilla.rb} +1 -1
  13. data/lib/licensee/matchers/{exact_matcher.rb → exact.rb} +2 -1
  14. data/lib/licensee/matchers/gemspec.rb +24 -0
  15. data/lib/licensee/matchers/{package_matcher.rb → matcher.rb} +5 -3
  16. data/lib/licensee/matchers/{npm_bower_matcher.rb → npm_bower.rb} +2 -2
  17. data/lib/licensee/matchers/package.rb +22 -0
  18. data/lib/licensee/project_files.rb +8 -0
  19. data/lib/licensee/project_files/license_file.rb +22 -4
  20. data/lib/licensee/project_files/package_manager_file.rb +37 -0
  21. data/lib/licensee/project_files/project_file.rb +65 -0
  22. data/lib/licensee/project_files/{readme.rb → readme_file.rb} +2 -2
  23. data/lib/licensee/projects.rb +7 -0
  24. data/lib/licensee/projects/fs_project.rb +64 -27
  25. data/lib/licensee/projects/git_project.rb +49 -43
  26. data/lib/licensee/projects/project.rb +149 -0
  27. data/lib/licensee/version.rb +1 -1
  28. data/spec/bin_spec.rb +9 -2
  29. data/spec/fixtures/crlf-license/LICENSE +674 -0
  30. data/spec/fixtures/fcpl-modified-mpl/LICENSE +193 -193
  31. data/spec/fixtures/ipsum.txt +19 -0
  32. data/spec/fixtures/license-in-parent-folder/LICENSE.txt +21 -0
  33. data/spec/fixtures/license-in-parent-folder/license-folder/LICENSE.txt +21 -0
  34. data/spec/fixtures/multiple-license-files/LICENSE +362 -0
  35. data/spec/fixtures/multiple-license-files/LICENSE.txt +21 -0
  36. data/spec/integration_spec.rb +57 -22
  37. data/spec/licensee/content_helper_spec.rb +33 -9
  38. data/spec/licensee/license_spec.rb +8 -1
  39. data/spec/licensee/matchers/cabal_matcher_spec.rb +45 -0
  40. data/spec/licensee/matchers/copyright_matcher_spec.rb +1 -1
  41. data/spec/licensee/matchers/cran_matcher_spec.rb +11 -2
  42. data/spec/licensee/matchers/dice_matcher_spec.rb +1 -2
  43. data/spec/licensee/matchers/dist_zilla_matcher_spec.rb +11 -2
  44. data/spec/licensee/matchers/exact_matcher_spec.rb +1 -1
  45. data/spec/licensee/matchers/gemspec_matcher_spec.rb +19 -1
  46. data/spec/licensee/matchers/npm_bower_matcher_spec.rb +23 -6
  47. data/spec/licensee/matchers/package_matcher_spec.rb +31 -2
  48. data/spec/licensee/project_files/license_file_spec.rb +62 -6
  49. data/spec/licensee/project_files/package_info_spec.rb +10 -1
  50. data/spec/{project_file_spec.rb → licensee/project_files/project_file_spec.rb} +14 -1
  51. data/spec/licensee/project_files/readme_spec.rb +1 -1
  52. data/spec/licensee/project_spec.rb +134 -9
  53. data/spec/licensee_spec.rb +4 -3
  54. data/spec/spec_helper.rb +12 -23
  55. data/spec/vendored_license_spec.rb +16 -13
  56. metadata +29 -17
  57. data/lib/licensee/matchers/gemspec_matcher.rb +0 -19
  58. data/lib/licensee/project.rb +0 -87
  59. data/lib/licensee/project_file.rb +0 -39
  60. data/lib/licensee/project_files/package_info.rb +0 -31
@@ -1,16 +1,18 @@
1
1
  module Licensee
2
2
  module Matchers
3
- class Package
3
+ class Matcher
4
+ attr_reader :file
5
+
4
6
  def initialize(file)
5
7
  @file = file
6
8
  end
7
9
 
8
10
  def match
9
- Licensee.licenses(hidden: true).find { |l| l.key == license_property }
11
+ raise 'Not implemented'
10
12
  end
11
13
 
12
14
  def confidence
13
- 90
15
+ raise 'Not implemented'
14
16
  end
15
17
  end
16
18
  end
@@ -1,10 +1,10 @@
1
1
  module Licensee
2
2
  module Matchers
3
- class NpmBower < Package
3
+ class NpmBower < Licensee::Matchers::Package
4
4
  # While we could parse the package.json or bower.json file, prefer
5
5
  # a lenient regex for speed and security. Moar parsing moar problems.
6
6
  LICENSE_REGEX = /
7
- s*[\"\']license[\"\']\s*\:\s*[\'\"]([a-z\-0-9\.]+)[\'\"],?\s*
7
+ \s*[\"\']license[\"\']\s*\:\s*[\'\"]([a-z\-0-9\.]+)[\'\"],?\s*
8
8
  /ix
9
9
 
10
10
  private
@@ -0,0 +1,22 @@
1
+ module Licensee
2
+ module Matchers
3
+ class Package < Licensee::Matchers::Matcher
4
+ def match
5
+ return @match if defined? @match
6
+ return if license_property.nil? || license_property.to_s.empty?
7
+ @match = Licensee.licenses(hidden: true).find do |license|
8
+ license.key == license_property
9
+ end
10
+ @match ||= License.find('other')
11
+ end
12
+
13
+ def confidence
14
+ 90
15
+ end
16
+
17
+ def license_property
18
+ raise 'Not implemented'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ module Licensee
2
+ module ProjectFiles
3
+ autoload :ProjectFile, 'licensee/project_files/project_file'
4
+ autoload :LicenseFile, 'licensee/project_files/license_file'
5
+ autoload :PackageManagerFile, 'licensee/project_files/package_manager_file'
6
+ autoload :ReadmeFile, 'licensee/project_files/readme_file'
7
+ end
8
+ end
@@ -1,6 +1,6 @@
1
1
  module Licensee
2
- class Project
3
- class LicenseFile < Licensee::Project::File
2
+ module ProjectFiles
3
+ class LicenseFile < Licensee::ProjectFiles::ProjectFile
4
4
  include Licensee::ContentHelper
5
5
 
6
6
  # List of extensions to give preference to
@@ -27,8 +27,10 @@ module Licensee
27
27
  /\A#{COPYING_REGEX}#{PREFERRED_EXT_REGEX}\z/ => 0.7, # COPYING.md
28
28
  /\A#{LICENSE_REGEX}#{ANY_EXT_REGEX}\z/ => 0.6, # LICENSE.textile
29
29
  /\A#{COPYING_REGEX}#{ANY_EXT_REGEX}\z/ => 0.5, # COPYING.textile
30
- /#{LICENSE_REGEX}/ => 0.4, # LICENSE-MIT
31
- /#{COPYING_REGEX}/ => 0.3, # COPYING-MIT
30
+ /#{LICENSE_REGEX}-/ => 0.4, # LICENSE-MIT
31
+ /#{COPYING_REGEX}-/ => 0.35, # COPYING-MIT
32
+ /-#{LICENSE_REGEX}/ => 0.3, # MIT-LICENSE-MIT
33
+ /-#{COPYING_REGEX}/ => 0.25, # MIT-COPYING
32
34
  /\A#{OFL_REGEX}#{PREFERRED_EXT_REGEX}/ => 0.2, # OFL.md
33
35
  /\A#{OFL_REGEX}#{ANY_EXT_REGEX}/ => 0.1, # OFL.textile
34
36
  /\A#{OFL_REGEX}\z/ => 0.05, # OFL
@@ -58,6 +60,22 @@ module Licensee
58
60
  content.strip =~ CC_FALSE_POSITIVE_REGEX
59
61
  end
60
62
 
63
+ def lgpl?
64
+ LicenseFile.lesser_gpl_score(filename) == 1 && license && license.lgpl?
65
+ end
66
+
67
+ def gpl?
68
+ license && license.gpl?
69
+ end
70
+
71
+ def license
72
+ if matcher && matcher.match
73
+ matcher.match
74
+ else
75
+ License.find('other')
76
+ end
77
+ end
78
+
61
79
  def self.name_score(filename)
62
80
  FILENAME_REGEXES.find { |regex, _| filename =~ regex }[1]
63
81
  end
@@ -0,0 +1,37 @@
1
+ module Licensee
2
+ module ProjectFiles
3
+ class PackageManagerFile < Licensee::ProjectFiles::ProjectFile
4
+ # Hash of Extension => [possible matchers]
5
+ MATCHERS_EXTENSIONS = {
6
+ '.gemspec' => [Matchers::Gemspec],
7
+ '.json' => [Matchers::NpmBower],
8
+ '.cabal' => [Matchers::Cabal]
9
+ }.freeze
10
+
11
+ # Hash of Filename => [possible matchers]
12
+ FILENAMES_EXTENSIONS = {
13
+ 'DESCRIPTION' => [Matchers::Cran],
14
+ 'dist.ini' => [Matchers::DistZilla]
15
+ }.freeze
16
+
17
+ def possible_matchers
18
+ MATCHERS_EXTENSIONS[extension] || FILENAMES_EXTENSIONS[filename] || []
19
+ end
20
+
21
+ def self.name_score(filename)
22
+ return 1.0 if ['.gemspec', '.cabal'].include?(File.extname(filename))
23
+ return 1.0 if filename == 'package.json'
24
+ return 0.8 if filename == 'dist.ini'
25
+ return 0.9 if filename == 'DESCRIPTION'
26
+ return 0.75 if filename == 'bower.json'
27
+ 0.0
28
+ end
29
+
30
+ private
31
+
32
+ def extension
33
+ @extension ||= File.extname(filename)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ # A project file is a file within a project that contains license information
2
+ # Currently extended by LicenseFile, PackageManagerFile, and ReadmeFile
3
+ #
4
+ # Sublcasses should implement the possible_matchers method
5
+ module Licensee
6
+ module ProjectFiles
7
+ class ProjectFile
8
+ extend Forwardable
9
+ def_delegator :@data, :[]
10
+
11
+ attr_reader :content
12
+
13
+ ENCODING = Encoding::UTF_8
14
+ ENCODING_OPTIONS = {
15
+ invalid: :replace,
16
+ undef: :replace,
17
+ replace: ''
18
+ }.freeze
19
+
20
+ # Create a new Licensee::ProjectFile with content and metadata
21
+ #
22
+ # content - file content
23
+ # metadata - can be either the string filename, or a hash containing
24
+ # metadata about the file content. If a hash is given, the
25
+ # filename should be given using the :name key. See individual
26
+ # project types for additional available metadata
27
+ #
28
+ # Returns a new Licensee::ProjectFile
29
+ def initialize(content, metadata = {})
30
+ @content = content
31
+ @content.force_encoding(ENCODING)
32
+ unless @content.valid_encoding?
33
+ @content.encode!(ENCODING, ENCODING_OPTIONS)
34
+ end
35
+
36
+ metadata = { name: metadata } if metadata.is_a? String
37
+ @data = metadata || {}
38
+ end
39
+
40
+ def filename
41
+ @data[:name]
42
+ end
43
+
44
+ def possible_matchers
45
+ raise 'Not implemented'
46
+ end
47
+
48
+ def matcher
49
+ @matcher ||= possible_matchers.map { |m| m.new(self) }.find(&:match)
50
+ end
51
+
52
+ # Returns the percent confident with the match
53
+ def confidence
54
+ matcher && matcher.confidence
55
+ end
56
+
57
+ def license
58
+ matcher && matcher.match
59
+ end
60
+
61
+ alias match license
62
+ alias path filename
63
+ end
64
+ end
65
+ end
@@ -1,6 +1,6 @@
1
1
  module Licensee
2
- class Project
3
- class Readme < LicenseFile
2
+ module ProjectFiles
3
+ class ReadmeFile < Licensee::ProjectFiles::LicenseFile
4
4
  SCORES = {
5
5
  /\AREADME\z/i => 1.0,
6
6
  /\AREADME\.(md|markdown|mdown|txt)\z/i => 0.9
@@ -0,0 +1,7 @@
1
+ module Licensee
2
+ module Projects
3
+ autoload :Project, 'licensee/projects/project'
4
+ autoload :FSProject, 'licensee/projects/fs_project'
5
+ autoload :GitProject, 'licensee/projects/git_project'
6
+ end
7
+ end
@@ -1,41 +1,78 @@
1
1
  # Filesystem-based project
2
2
  #
3
3
  # Analyze a folder on the filesystem for license information
4
+ #
5
+ # Project files for this project type will contain the following keys:
6
+ # :name - the relative file name
7
+ # :dir - the directory path containing the file
4
8
  module Licensee
5
- class FSProject < Project
6
- def initialize(path, **args)
7
- if ::File.file?(path)
8
- @pattern = ::File.basename(path)
9
- @dir = ::File.dirname(path)
10
- else
11
- @pattern = '*'
12
- @dir = path
9
+ module Projects
10
+ class FSProject < Licensee::Projects::Project
11
+ def initialize(path, **args)
12
+ if ::File.file?(path)
13
+ @pattern = ::File.basename(path)
14
+ @dir = ::File.dirname(path)
15
+ else
16
+ @pattern = '*'
17
+ @dir = path
18
+ end
19
+
20
+ @root = args.delete(:search_root) || @dir
21
+ unless valid_search_root?
22
+ raise 'Search root must be the project path directory or its ancestor'
23
+ end
24
+
25
+ super(**args)
13
26
  end
14
- super(**args)
15
- end
16
27
 
17
- private
28
+ private
29
+
30
+ # Returns an array of hashes representing the project's files.
31
+ # Hashes will have the the following keys:
32
+ # :name - the relative file name
33
+ # :dir - the directory path containing the file
34
+ def files
35
+ @files ||= search_directories.flat_map do |dir|
36
+ Dir.glob(::File.join(dir, @pattern).tr('\\', '/')).map do |file|
37
+ next unless ::File.file?(file)
38
+ { name: ::File.basename(file), dir: dir }
39
+ end.compact
40
+ end
41
+ end
18
42
 
19
- # Returns an array of hashes representing the project's files.
20
- # Hashes will have the :name key, with the relative path to the file
21
- def files
22
- files = []
43
+ # Retrieve a file's content from disk
44
+ #
45
+ # file - the file hash, with the :name key as the file's relative path
46
+ #
47
+ # Returns the file contents as a string
48
+ def load_file(file)
49
+ ::File.read(::File.join(file[:dir], file[:name]))
50
+ end
23
51
 
24
- Dir.glob(::File.join(@dir, @pattern).tr('\\', '/')) do |file|
25
- next unless ::File.file?(file)
26
- files.push(name: ::File.basename(file))
52
+ # Returns true if @dir is @root or it's descendant
53
+ def valid_search_root?
54
+ dir = Pathname.new(@dir)
55
+ dir.fnmatch?(@root) || dir.fnmatch?(::File.join(@root, '**'))
27
56
  end
28
57
 
29
- files
30
- end
58
+ # Returns the set of unique paths to search for project files
59
+ # in order from @dir -> @root
60
+ def search_directories
61
+ search_enumerator.map(&:to_path)
62
+ .push(@root) # ensure root is included in the search
63
+ .uniq # don't include the root twice if @dir == @root
64
+ end
31
65
 
32
- # Retrieve a file's content from disk
33
- #
34
- # file - the file hash, with the :name key as the file's relative path
35
- #
36
- # Returns the file contents as a string
37
- def load_file(file)
38
- ::File.read(::File.join(@dir, file[:name]))
66
+ # Enumerates all directories to search, from @dir to @root
67
+ def search_enumerator
68
+ root = Pathname.new(@root)
69
+ dir = Pathname.new(@dir)
70
+ Enumerator.new do |yielder|
71
+ dir.relative_path_from(root).ascend do |relative|
72
+ yielder.yield root.join(relative)
73
+ end
74
+ end
75
+ end
39
76
  end
40
77
  end
41
78
  end
@@ -1,60 +1,66 @@
1
1
  # Git-based project
2
2
  #
3
3
  # Analyze a given (bare) Git repository for license information
4
+ #
5
+ # Project files for this project type will contain the following keys:
6
+ # :name - the file's path relative to the repo root
7
+ # :oid - the file's OID
4
8
  module Licensee
5
- class GitProject < Project
6
- attr_reader :repository, :revision
9
+ module Projects
10
+ class GitProject < Licensee::Projects::Project
11
+ attr_reader :repository, :revision
7
12
 
8
- class InvalidRepository < ArgumentError; end
13
+ class InvalidRepository < ArgumentError; end
9
14
 
10
- def initialize(repo, revision: nil, **args)
11
- @repository = if repo.is_a? Rugged::Repository
12
- repo
13
- else
14
- Rugged::Repository.new(repo)
15
- end
15
+ def initialize(repo, revision: nil, **args)
16
+ @repository = if repo.is_a? Rugged::Repository
17
+ repo
18
+ else
19
+ Rugged::Repository.new(repo)
20
+ end
16
21
 
17
- @revision = revision
18
- super(**args)
19
- rescue Rugged::OSError, Rugged::RepositoryError
20
- raise InvalidRepository
21
- end
22
+ @revision = revision
23
+ super(**args)
24
+ rescue Rugged::OSError, Rugged::RepositoryError
25
+ raise InvalidRepository
26
+ end
22
27
 
23
- def close
24
- @repository.close
25
- end
28
+ def close
29
+ @repository.close
30
+ end
26
31
 
27
- private
32
+ private
28
33
 
29
- def commit
30
- @commit ||= if revision
31
- repository.lookup(revision)
32
- else
33
- repository.last_commit
34
+ def commit
35
+ @commit ||= if revision
36
+ repository.lookup(revision)
37
+ else
38
+ repository.last_commit
39
+ end
34
40
  end
35
- end
36
41
 
37
- MAX_LICENSE_SIZE = 64 * 1024
42
+ MAX_LICENSE_SIZE = 64 * 1024
38
43
 
39
- # Retrieve a file's content from the Git database
40
- #
41
- # file - the file hash, including the file's OID
42
- #
43
- # Returns a string representing the file's contents
44
- def load_file(file)
45
- data, = Rugged::Blob.to_buffer(repository, file[:oid], MAX_LICENSE_SIZE)
46
- data
47
- end
44
+ # Retrieve a file's content from the Git database
45
+ #
46
+ # file - the file hash, including the file's OID
47
+ #
48
+ # Returns a string representing the file's contents
49
+ def load_file(file)
50
+ data, = Rugged::Blob.to_buffer(repository, file[:oid], MAX_LICENSE_SIZE)
51
+ data
52
+ end
48
53
 
49
- # Returns an array of hashes representing the project's files.
50
- # Hashes will have the the following keys:
51
- # :name - the file's path relative to the repo root
52
- # :oid - the file's OID
53
- def files
54
- commit.tree.map do |entry|
55
- next unless entry[:type] == :blob
56
- { name: entry[:name], oid: entry[:oid] }
57
- end.compact
54
+ # Returns an array of hashes representing the project's files.
55
+ # Hashes will have the the following keys:
56
+ # :name - the file's path relative to the repo root
57
+ # :oid - the file's OID
58
+ def files
59
+ @files ||= commit.tree.map do |entry|
60
+ next unless entry[:type] == :blob
61
+ { name: entry[:name], oid: entry[:oid] }
62
+ end.compact
63
+ end
58
64
  end
59
65
  end
60
66
  end