licensee 8.9.2 → 9.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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