license_conflicts 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71e417d8704344ac7126fa69287e32f7b13f7a4a67f8ef6fdb1bb0cfee5efdb9
4
- data.tar.gz: e9f2fe086f42d0e7ecb76931a8017ff4e9234fd31e88263d16f34c28469fe15e
3
+ metadata.gz: 6eb6707c39f32730f7a721952ab41d3995e5b75b33b69147dd8a6ccb403b4540
4
+ data.tar.gz: e555affa56259102318068dbdc4fd7cd27a832c2fdd682e6c55f06275e729265
5
5
  SHA512:
6
- metadata.gz: 8e1000e39b9851c607324351e1df775725dcb5179453ec771a3480cb1162da80c1d86a1f19189bce9ff4d397bce58ab142509cdce41d9aef181fadd7a6296a9b
7
- data.tar.gz: 8441600041254fea52ef4809efe234a70283bcf077efe2f4cf4a1d1cbbb2f7bc3ccf5f5608954e210207759260cd042384bbcea4dc200a5f45d782a2975b9e9b
6
+ metadata.gz: 02f04fd29206aaa303a33c40b6e65d72e4c34d5849fb1ccbde055450ca19a071501e7b4a15cb3ae02cf366b9872ff30d91a42d9bb0b9df014e4712c359973978
7
+ data.tar.gz: 1a7c1ff94d28ba7e41fec874177dc5d2f4740eb568acdacd16bcff36e05d2ca2ecf36685e6ef54e2cd826e111261dd8071490ab00de2f0ee00901f8481b8da36
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.3.1"
2
+ ".": "0.4.0"
3
3
  }
data/.rubocop.yml CHANGED
@@ -11,3 +11,9 @@ Style/StringLiteralsInInterpolation:
11
11
 
12
12
  Layout/LineLength:
13
13
  Max: 120
14
+
15
+ # RSpec specs use a single `describe` block per file, naturally above the default
16
+ # line limit; the limit does not apply to them.
17
+ Metrics/BlockLength:
18
+ Exclude:
19
+ - "spec/**/*"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.2...license_conflicts/v0.4.0) (2026-11-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * analyze only direct dependencies ([1851332](https://github.com/dennisurtubia/license_conflicts/commit/1851332351dd0cc2da1f98124a1ab31749e1dd70))
9
+ * analyze only direct dependencies ([a299cc2](https://github.com/dennisurtubia/license_conflicts/commit/a299cc21e4fd6105ebc03cc3d22ac14476d7fca5))
10
+
11
+ ## [0.3.2](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.1...license_conflicts/v0.3.2) (2026-09-13)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * conflicts mapping ([afcab97](https://github.com/dennisurtubia/license_conflicts/commit/afcab97686ffaa1351054667e027a683c64b78be))
17
+ * conflicts mapping ([4e4cd3a](https://github.com/dennisurtubia/license_conflicts/commit/4e4cd3abc017181e4d45a35cbcad376d6e359af5))
18
+
3
19
  ## [0.3.1](https://github.com/dennisurtubia/license_conflicts/compare/license_conflicts/v0.3.0...license_conflicts/v0.3.1) (2026-03-21)
4
20
 
5
21
 
data/README.md CHANGED
@@ -23,11 +23,35 @@
23
23
  ## How It Works
24
24
 
25
25
  1. **Detects the project license** from its metadata file (`package.json`, `gemspec`, `pom.xml`, etc.)
26
- 2. **Scans all dependencies** using LicenseFinder
26
+ 2. **Scans the dependencies** using LicenseFinder (see [Direct dependencies only](#direct-dependencies-only))
27
27
  3. **Normalizes license names** — SPDX identifiers, common aliases, and variant spellings are all mapped to canonical names
28
28
  4. **Checks for conflicts** using a built-in compatibility matrix
29
29
  5. **Reports results** to stdout and exits with an appropriate code
30
30
 
31
+ ### Direct dependencies only
32
+
33
+ Only the **direct** dependencies are analyzed: the ones the project declares in its own manifests
34
+ at the project root. Transitive dependencies that LicenseFinder also reports are left out.
35
+
36
+ | Manifest | Declarations read |
37
+ |--------------------------------|-------------------------------------------------------------------------|
38
+ | `package.json`, `bower.json` | `dependencies`, `devDependencies` |
39
+ | `Gemfile`, `*.gemspec` | `gem`, `add_dependency`, `add_runtime_dependency`, `add_development_dependency` |
40
+ | `requirements.txt` | every requirement line (`-r`/`-e` options and bare URLs are skipped) |
41
+ | `setup.cfg` | `[options] install_requires` |
42
+ | `pyproject.toml` | `[project] dependencies`; Poetry dependency tables (`python` excluded) |
43
+ | `go.mod` | `require` directives not marked `// indirect` |
44
+ | `pom.xml` | `<dependencies>` (by `artifactId` and `groupId:artifactId`) |
45
+
46
+ Every manifest present contributes. Names are compared ignoring case and the `-`, `_` and `.`
47
+ separators. When a package manager reports a declared name in several versions, the one
48
+ matching the copy at its install path is checked. Declared dependencies that were not resolved
49
+ are not counted. If no manifest above is present (for example, a project that only has
50
+ `Godeps/Godeps.json`, which pins every vendored package without telling direct from transitive
51
+ ones), every dependency LicenseFinder reports is analyzed.
52
+
53
+ Transitive dependencies are a known limitation, not yet covered.
54
+
31
55
  ---
32
56
 
33
57
  ## Supported Languages
@@ -47,7 +71,9 @@
47
71
  ## Requirements
48
72
 
49
73
  - Ruby >= 2.6.0
50
- - The package manager for your project type must be installed (e.g. `npm`, `bundler`, `mvn`)
74
+ - The package manager for your project type must be installed (e.g. `npm`, `bundler`, `mvn`).
75
+ LicenseFinder activates every package manager whose manifest it finds at the project root, so
76
+ a repository that also ships, say, a `composer.json` needs `composer` too (otherwise exit `2`).
51
77
 
52
78
  ---
53
79
 
@@ -97,7 +123,7 @@ Results are printed to **stdout** as a single comma-separated line:
97
123
 
98
124
  | Field | Description |
99
125
  |-----------------------|----------------------------------------------------------------------------------|
100
- | `dependency_count` | Number of scanned dependencies |
126
+ | `dependency_count` | Number of analyzed (direct) dependencies |
101
127
  | `project_license` | Detected license of the analyzed project |
102
128
  | `conflicting_licenses`| Semicolon-separated list of incompatible licenses found (empty if none) |
103
129
  | `report` | Full dependency report (only present when `--format` is specified) |
@@ -1,52 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # All license names in this file use the canonical form returned by
4
- # LicenseFinder (dependency.licenses.first.name). Input is normalised via
5
- # LicenseNormalizer before any lookup, so SPDX IDs and common aliases are
6
- # handled transparently.
7
-
3
+ # Key: declared license of the EXAMINED PROJECT.
4
+ # Value: DEPENDENCY licenses that conflict with it, i.e. licenses that
5
+ # CANNOT be relicensed/combined under the project's license.
6
+ #
7
+ # Directional semantics (Kapitsaki et al. 2017, Definition 1 and Fig. 2):
8
+ # conflict(project P, dependency D) <=> there is no path D -> P in the
9
+ # article's compatibility graph.
10
+ #
11
+ # Documented assumptions/limitations:
12
+ # - Single-licensed projects only (OR/AND multi-licensing is out of scope).
13
+ # - Flattened names treated as the exact version ("only"): "GPLv2" = GPL-2.0-only, etc.
14
+ # - Canonical names as returned by LicenseFinder, normalized via LicenseNormalizer.
15
+ #
16
+ # rubocop:disable Metrics/ModuleLength -- hardcoded data map, long by nature.
8
17
  module LicenseConflicts
9
- APACHE2_CONFLICTS = [
10
- "MIT",
11
- "New BSD",
12
- "Simplified BSD",
13
- "Zlib",
14
- "MPL 1.1",
15
- "CDDL 1.0",
16
- "AGPL 1.0"
17
- ].freeze
18
-
19
- NEW_BSD_CONFLICTS = [
20
- "MIT",
21
- "Simplified BSD",
22
- "Zlib",
23
- "MPL 1.1",
24
- "CDDL 1.0",
25
- "AGPL 1.0"
26
- ].freeze
27
-
28
- GPL2_CONFLICTS = [
18
+ ALL_LICENSES = [
29
19
  "MIT",
30
20
  "Simplified BSD",
31
21
  "New BSD",
32
- "Apache 2.0",
33
22
  "Zlib",
34
- "AFL 3.0",
35
- "MPL 1.1",
36
- "MPL 2.0",
37
- "CDDL 1.0",
38
- "LGPL 2.1",
39
- "LGPL 3.0",
40
- "OSL 3.0",
41
- "AGPL 1.0"
42
- ].freeze
43
-
44
- GPL3_CONFLICTS = [
45
- "MIT",
46
- "Simplified BSD",
47
- "New BSD",
48
23
  "Apache 2.0",
49
- "Zlib",
50
24
  "AFL 3.0",
51
25
  "MPL 1.1",
52
26
  "MPL 2.0",
@@ -55,114 +29,101 @@ module LicenseConflicts
55
29
  "LGPL 3.0",
56
30
  "OSL 3.0",
57
31
  "GPLv2",
58
- "AGPL 1.0"
32
+ "GPLv3",
33
+ "AGPL 1.0",
34
+ "AGPL 3"
59
35
  ].freeze
60
36
 
61
- MPL2_CONFLICTS = [
62
- "MIT",
63
- "Simplified BSD",
64
- "New BSD",
65
- "Apache 2.0",
66
- "Zlib",
67
- "AFL 3.0",
68
- "MPL 1.1",
69
- "CDDL 1.0",
70
- "LGPL 3.0",
71
- "OSL 3.0",
72
- "AGPL 1.0"
73
- ].freeze
37
+ CONFLICTS_MAP = {
38
+ # Only MIT code (and public domain) can be relicensed as MIT.
39
+ "MIT" => (ALL_LICENSES - ["MIT"]).freeze,
74
40
 
75
- SIMPLIFIED_BSD_CONFLICTS = [
76
- "MIT",
77
- "Zlib",
78
- "MPL 1.1",
79
- "CDDL 1.0",
80
- "AGPL 1.0"
81
- ].freeze
41
+ "Simplified BSD" => (ALL_LICENSES - ["MIT", "Simplified BSD"]).freeze,
82
42
 
83
- CONFLICTS_MAP = {
84
- "MIT" => [
85
- "Zlib",
86
- "MPL 1.1",
87
- "CDDL 1.0",
88
- "AGPL 1.0"
89
- ],
90
- "Simplified BSD" => SIMPLIFIED_BSD_CONFLICTS,
91
- "New BSD" => NEW_BSD_CONFLICTS,
92
- "Apache 2.0" => APACHE2_CONFLICTS,
93
- "Zlib" => [
94
- "MIT",
95
- "New BSD",
96
- "Simplified BSD",
43
+ "New BSD" => (ALL_LICENSES - ["MIT", "Simplified BSD", "New BSD"]).freeze,
44
+
45
+ # No other license flows into Zlib in the article's graph.
46
+ "Zlib" => (ALL_LICENSES - ["Zlib"]).freeze,
47
+
48
+ # Permissive licenses (MIT, BSDs, Zlib) flow into Apache-2.0; everything else conflicts.
49
+ "Apache 2.0" => [
50
+ "AFL 3.0",
97
51
  "MPL 1.1",
52
+ "MPL 2.0",
98
53
  "CDDL 1.0",
99
- "AGPL 1.0"
100
- ],
54
+ "LGPL 2.1",
55
+ "LGPL 3.0",
56
+ "OSL 3.0",
57
+ "GPLv2",
58
+ "GPLv3",
59
+ "AGPL 1.0",
60
+ "AGPL 3"
61
+ ].freeze,
62
+
101
63
  "AFL 3.0" => [
102
- "MIT",
103
- "Simplified BSD",
104
- "New BSD",
105
- "Apache 2.0",
106
64
  "MPL 1.1",
107
65
  "MPL 2.0",
108
66
  "CDDL 1.0",
109
67
  "LGPL 2.1",
110
68
  "LGPL 3.0",
69
+ "OSL 3.0",
111
70
  "GPLv2",
112
71
  "GPLv3",
113
- "AGPL 3",
114
- "Zlib",
115
- "AGPL 1.0"
116
- ],
117
- "MPL 1.1" => [
118
- "MIT",
119
- "Simplified BSD",
120
- "New BSD",
121
- "Apache 2.0",
122
- "Zlib",
72
+ "AGPL 1.0",
73
+ "AGPL 3"
74
+ ].freeze,
75
+
76
+ # No other license flows into MPL-1.1.
77
+ "MPL 1.1" => (ALL_LICENSES - ["MPL 1.1"]).freeze,
78
+
79
+ # MIT/BSDs (solid edge), Zlib/Apache-2.0 and MPL-1.1 (non-transitive edges as the
80
+ # last edge of the path) flow into MPL-2.0. Article's py2exe case: MIT + MPL-1.1
81
+ # are combinable under MPL-2.0.
82
+ "MPL 2.0" => [
123
83
  "AFL 3.0",
84
+ "CDDL 1.0",
85
+ "LGPL 2.1",
124
86
  "LGPL 3.0",
125
87
  "OSL 3.0",
126
- "AGPL 1.0"
127
- ],
128
- "MPL 2.0" => MPL2_CONFLICTS,
129
- "CDDL 1.0" => [
130
- "MIT",
131
- "Simplified BSD",
132
- "New BSD",
133
- "Apache 2.0",
88
+ "GPLv2",
89
+ "GPLv3",
90
+ "AGPL 1.0",
91
+ "AGPL 3"
92
+ ].freeze,
93
+
94
+ # Only MPL-1.1 flows into CDDL-1.0 (CDDL is derived from MPL-1.1).
95
+ "CDDL 1.0" => (ALL_LICENSES - ["MPL 1.1", "CDDL 1.0"]).freeze,
96
+
97
+ # Zlib and Apache-2.0 do NOT flow into LGPL-2.1 (explicit incompatibility in the
98
+ # article -- Shopware case). GPL cannot flow "back" into LGPL.
99
+ "LGPL 2.1" => [
134
100
  "Zlib",
101
+ "Apache 2.0",
135
102
  "AFL 3.0",
136
103
  "MPL 1.1",
137
- "MPL 2.0",
138
- "LGPL 2.1",
104
+ "CDDL 1.0",
139
105
  "LGPL 3.0",
140
106
  "OSL 3.0",
141
107
  "GPLv2",
142
108
  "GPLv3",
143
- "AGPL 3",
144
- "AGPL 1.0"
145
- ],
146
- "LGPL 2.1" => [
147
- "MIT",
148
- "Simplified BSD",
149
- "New BSD",
150
- "Apache 2.0",
151
- "Zlib",
109
+ "AGPL 1.0",
110
+ "AGPL 3"
111
+ ].freeze,
112
+
113
+ # Apache-2.0 and Zlib flow into LGPL-3.0 (unlike LGPL-2.1).
114
+ "LGPL 3.0" => [
152
115
  "AFL 3.0",
153
116
  "MPL 1.1",
154
- "MPL 2.0",
155
117
  "CDDL 1.0",
118
+ "LGPL 2.1",
156
119
  "OSL 3.0",
157
- "AGPL 1.0"
158
- ],
120
+ "GPLv2",
121
+ "GPLv3",
122
+ "AGPL 1.0",
123
+ "AGPL 3"
124
+ ].freeze,
125
+
159
126
  "OSL 3.0" => [
160
- "MIT",
161
- "Simplified BSD",
162
- "New BSD",
163
- "Apache 2.0",
164
- "Zlib",
165
- "AFL 3.0",
166
127
  "MPL 1.1",
167
128
  "MPL 2.0",
168
129
  "CDDL 1.0",
@@ -170,43 +131,49 @@ module LicenseConflicts
170
131
  "LGPL 3.0",
171
132
  "GPLv2",
172
133
  "GPLv3",
173
- "AGPL 3",
174
- "AGPL 1.0"
175
- ],
176
- "GPLv2" => GPL2_CONFLICTS,
177
- "GPLv3" => GPL3_CONFLICTS,
178
- "AGPL 3" => [
179
- "MIT",
180
- "Simplified BSD",
181
- "New BSD",
182
- "Apache 2.0",
134
+ "AGPL 1.0",
135
+ "AGPL 3"
136
+ ].freeze,
137
+
138
+ # Apache-2.0 and Zlib are incompatible with GPL-2.0 (explicit examples in the
139
+ # article, Section 4.1 and Table 4). GPLv3 incompatible with GPLv2 "and vice versa".
140
+ "GPLv2" => [
183
141
  "Zlib",
142
+ "Apache 2.0",
184
143
  "AFL 3.0",
185
144
  "MPL 1.1",
186
- "MPL 2.0",
187
145
  "CDDL 1.0",
188
- "LGPL 2.1",
189
146
  "LGPL 3.0",
190
147
  "OSL 3.0",
191
- "GPLv2",
192
148
  "GPLv3",
193
- "AGPL 1.0"
194
- ],
195
- "AGPL 1.0" => [
196
- "MIT",
197
- "Simplified BSD",
198
- "New BSD",
199
- "Apache 2.0",
200
- "Zlib",
149
+ "AGPL 1.0",
150
+ "AGPL 3"
151
+ ].freeze,
152
+
153
+ # Apache-2.0, Zlib, MPL-2.0 and the LGPLs flow into GPL-3.0.
154
+ "GPLv3" => [
201
155
  "AFL 3.0",
202
156
  "MPL 1.1",
203
- "MPL 2.0",
204
157
  "CDDL 1.0",
205
- "LGPL 2.1",
206
- "LGPL 3.0",
207
158
  "OSL 3.0",
208
159
  "GPLv2",
209
- "GPLv3"
210
- ]
160
+ "AGPL 1.0",
161
+ "AGPL 3"
162
+ ].freeze,
163
+
164
+ # Exact-only assumption: nothing flows into AGPL-1.0 (in the article's graph the
165
+ # node is AGPL-1.0+, with an outgoing edge only to AGPL-3.0).
166
+ "AGPL 1.0" => (ALL_LICENSES - ["AGPL 1.0"]).freeze,
167
+
168
+ # GPL-3.0 flows into AGPL-3.0 (GPLv3 clause 13); GPL-2.0 does not (Odoo case).
169
+ "AGPL 3" => [
170
+ "AFL 3.0",
171
+ "MPL 1.1",
172
+ "CDDL 1.0",
173
+ "OSL 3.0",
174
+ "GPLv2",
175
+ "AGPL 1.0"
176
+ ].freeze
211
177
  }.freeze
212
178
  end
179
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+
6
+ module LicenseConflicts
7
+ # Names of the dependencies a project declares in its own manifests at the project root,
8
+ # i.e. its direct dependencies. Every supported manifest present contributes to the set.
9
+ class DeclaredDependencies
10
+ # Manifest (glob relative to the project root) => reader returning the declared names, or
11
+ # nil when the file cannot be parsed. Godeps/Godeps.json is not listed: it pins every
12
+ # vendored package without telling direct from transitive ones.
13
+ READERS = {
14
+ "package.json" => :json_manifest_names,
15
+ "bower.json" => :json_manifest_names,
16
+ "*.gemspec" => :gemspec_names,
17
+ "Gemfile" => :gemfile_names,
18
+ "requirements.txt" => :requirements_names,
19
+ "setup.cfg" => :setup_cfg_names,
20
+ "pyproject.toml" => :pyproject_names,
21
+ "go.mod" => :go_mod_names,
22
+ "pom.xml" => :pom_names
23
+ }.freeze
24
+
25
+ # Some registries treat names differing only in case or in `-`, `_` and `.` as the same
26
+ # package, and package managers may report a name normalized differently from how the
27
+ # manifest spells it; names are compared by this key.
28
+ def self.key(name)
29
+ name.to_s.downcase.gsub(/[-_.]+/, "-")
30
+ end
31
+
32
+ def initialize(root = ".")
33
+ @root = root
34
+ end
35
+
36
+ # Set of normalized names, or nil when no readable manifest is present (direct and
37
+ # transitive dependencies cannot be told apart).
38
+ def names
39
+ return @names if defined?(@names)
40
+
41
+ declared = READERS.flat_map do |pattern, reader|
42
+ Dir.glob(File.join(@root, pattern)).map { |file| send(reader, File.read(file)) }
43
+ end.compact
44
+ @names = declared.empty? ? nil : declared.flatten.to_set { |name| self.class.key(name) }
45
+ end
46
+
47
+ def include?(name)
48
+ names&.include?(self.class.key(name))
49
+ end
50
+
51
+ private
52
+
53
+ # Requirement specifiers such as `requests[security]>=2.0; python_version > "3"`.
54
+ def requirement_name(specifier)
55
+ specifier[/\A\s*([A-Za-z0-9][A-Za-z0-9._-]*)/, 1]
56
+ end
57
+
58
+ def json_manifest_names(content)
59
+ data = JSON.parse(content)
60
+ return [] unless data.is_a?(Hash)
61
+
62
+ %w[dependencies devDependencies].flat_map { |field| data[field].is_a?(Hash) ? data[field].keys : [] }
63
+ rescue JSON::ParserError
64
+ nil
65
+ end
66
+
67
+ def gemspec_names(content)
68
+ content.scan(/\.add_(?:runtime_|development_)?dependency\s*\(?\s*["']([^"']+)["']/).flatten
69
+ end
70
+
71
+ def gemfile_names(content)
72
+ content.scan(/^\s*gem\s*\(?\s*["']([^"']+)["']/).flatten
73
+ end
74
+
75
+ def requirements_names(content)
76
+ content.each_line.filter_map do |line|
77
+ line = line.sub(/(?:\A|\s)#(?!egg=).*/, "").strip
78
+ next line[/#egg=([A-Za-z0-9._-]+)/, 1] if line.include?("://")
79
+ next if line.empty? || line.start_with?("-")
80
+
81
+ requirement_name(line)
82
+ end
83
+ end
84
+
85
+ def setup_cfg_names(content)
86
+ value = content[/^install_requires\s*=(.*(?:\n[ \t]+.*)*)/, 1]
87
+ return [] unless value
88
+
89
+ value.split("\n").filter_map { |line| requirement_name(line) }
90
+ end
91
+
92
+ def pyproject_names(content)
93
+ require "tomlrb"
94
+ data = Tomlrb.parse(content)
95
+ pep621 = Array(data.dig("project", "dependencies")).filter_map { |specifier| requirement_name(specifier) }
96
+ pep621 + poetry_names(data.dig("tool", "poetry") || {})
97
+ rescue LoadError, Tomlrb::ParseError
98
+ nil
99
+ end
100
+
101
+ def poetry_names(poetry)
102
+ groups = (poetry["group"] || {}).values.map { |group| group["dependencies"] }
103
+ tables = [poetry["dependencies"], poetry["dev-dependencies"], *groups]
104
+ tables.flat_map { |table| table.is_a?(Hash) ? table.keys : [] } - ["python"]
105
+ end
106
+
107
+ # `require` directives, single or in a block, except the ones marked `// indirect`.
108
+ def go_mod_names(content)
109
+ block_lines = content.scan(/^require\s*\(\s*\n(.*?)^\)/m).flatten.flat_map(&:lines)
110
+ single_lines = content.scan(/^require\s+([^\s(].*)$/).flatten
111
+ (block_lines + single_lines).reject { |line| line.include?("// indirect") }
112
+ .filter_map { |line| line.strip.split.first unless line.strip.start_with?("//") }
113
+ end
114
+
115
+ def pom_names(content)
116
+ require "rexml/document"
117
+ path = %w[project dependencies dependency].map { |tag| "*[local-name()='#{tag}']" }.join("/")
118
+ REXML::XPath.match(REXML::Document.new(content), "/#{path}").flat_map { |node| pom_coordinates(node) }
119
+ rescue REXML::ParseException
120
+ nil
121
+ end
122
+
123
+ # Both `artifactId` and `groupId:artifactId`, since the package manager may report either.
124
+ def pom_coordinates(dependency)
125
+ artifact, group = %w[artifactId groupId].map { |tag| REXML::XPath.first(dependency, "*[local-name()='#{tag}']")&.text }
126
+ return [] unless artifact
127
+
128
+ group ? [artifact, "#{group}:#{artifact}"] : [artifact]
129
+ end
130
+ end
131
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'license_finder'
4
4
  require 'license_conflicts/conflicts_map'
5
+ require "license_conflicts/declared_dependencies"
5
6
  require 'license_conflicts/license_normalizer'
6
7
  require 'license_conflicts/project_metadata'
7
8
 
@@ -23,7 +24,7 @@ module LicenseConflicts
23
24
  end
24
25
 
25
26
  def dependencies_count
26
- unapproved.count { |d| d.name != project_name }
27
+ dependencies.count
27
28
  end
28
29
 
29
30
  def project_license
@@ -56,8 +57,30 @@ module LicenseConflicts
56
57
  @unapproved ||= finder.unapproved
57
58
  end
58
59
 
60
+ # Only the direct dependencies, the ones the project declares in its own manifests, are
61
+ # analysed; transitive dependencies that LicenseFinder also reports are left out. When no
62
+ # supported manifest is found, every dependency LicenseFinder reports is kept.
63
+ def dependencies
64
+ @dependencies ||= begin
65
+ others = unapproved.reject { |d| d.name == project_name }
66
+ declared = DeclaredDependencies.new
67
+ declared.names ? one_per_name(others.select { |d| declared.include?(d.name) }) : others
68
+ end
69
+ end
70
+
71
+ # A package manager may report several versions of the same name (the direct dependency and
72
+ # transitive copies); keep the one whose version matches the copy at its install path.
73
+ def one_per_name(packages)
74
+ packages.group_by { |d| DeclaredDependencies.key(d.name) }.map do |_key, versions|
75
+ next versions.first if versions.one?
76
+
77
+ versions.find { |d| d.install_path && ProjectMetadata.new(d.install_path.to_s).version == d.version } ||
78
+ versions.first
79
+ end
80
+ end
81
+
59
82
  def check_conflicts
60
- unapproved.filter { |dependency| has_conflict?(dependency.licenses.first&.name) }
83
+ dependencies.filter { |dependency| has_conflict?(dependency.licenses.first&.name) }
61
84
  end
62
85
 
63
86
  def has_conflict?(dependency_license)
@@ -4,7 +4,12 @@ require "json"
4
4
  require "pathname"
5
5
 
6
6
  module LicenseConflicts
7
+ # rubocop:disable Metrics/ClassLength -- one small reader per supported manifest format.
7
8
  class ProjectMetadata
9
+ def initialize(root = ".")
10
+ @root = root
11
+ end
12
+
8
13
  def name
9
14
  @name ||= read_name
10
15
  end
@@ -13,6 +18,12 @@ module LicenseConflicts
13
18
  @license ||= read_license
14
19
  end
15
20
 
21
+ # Version declared by the manifest, used to identify which of several reported versions of a
22
+ # package is the one installed at its install path.
23
+ def version
24
+ @version ||= read_version
25
+ end
26
+
16
27
  private
17
28
 
18
29
  def read_name
@@ -24,7 +35,7 @@ module LicenseConflicts
24
35
  go_mod_data&.dig(:name) ||
25
36
  godeps_data&.dig(:name) ||
26
37
  pom_xml_data&.dig(:name) ||
27
- Pathname.pwd.basename.to_s
38
+ Pathname(@root).expand_path.basename.to_s
28
39
  end
29
40
 
30
41
  def read_license
@@ -36,74 +47,86 @@ module LicenseConflicts
36
47
  pom_xml_data&.dig(:license)
37
48
  end
38
49
 
50
+ def read_version
51
+ [package_json_data, bower_json_data].filter_map { |data| data&.dig("version") }.first ||
52
+ [gemspec_data, setup_cfg_data, pyproject_toml_data, pom_xml_data]
53
+ .filter_map { |data| data&.dig(:version) }.first
54
+ end
55
+
56
+ def path(file)
57
+ File.join(@root, file)
58
+ end
59
+
39
60
  def package_json_data
40
- return unless File.exist?("./package.json")
61
+ return unless File.exist?(path("package.json"))
41
62
 
42
- @package_json_data ||= JSON.parse(File.read("./package.json"))
63
+ @package_json_data ||= JSON.parse(File.read(path("package.json")))
43
64
  rescue JSON::ParserError
44
65
  nil
45
66
  end
46
67
 
47
68
  def bower_json_data
48
- return unless File.exist?("./bower.json")
69
+ return unless File.exist?(path("bower.json"))
49
70
 
50
- @bower_json_data ||= JSON.parse(File.read("./bower.json"))
71
+ @bower_json_data ||= JSON.parse(File.read(path("bower.json")))
51
72
  rescue JSON::ParserError
52
73
  nil
53
74
  end
54
75
 
55
76
  def gemspec_data
56
- gemspec_file = Dir.glob("./*.gemspec").first
77
+ gemspec_file = Dir.glob(path("*.gemspec")).first
57
78
  return unless gemspec_file
58
79
 
59
80
  @gemspec_data ||= begin
60
81
  content = File.read(gemspec_file)
61
82
  name = content[/\.name\s*=\s*["']([^"']+)["']/, 1]
62
83
  license = content[/\.license\s*=\s*["']([^"']+)["']/, 1]
63
- { name: name, license: license }
84
+ version = content[/\.version\s*=\s*["']([^"']+)["']/, 1]
85
+ { name: name, license: license, version: version }
64
86
  end
65
87
  end
66
88
 
67
89
  def setup_cfg_data
68
- return unless File.exist?("./setup.cfg")
90
+ return unless File.exist?(path("setup.cfg"))
69
91
 
70
92
  @setup_cfg_data ||= begin
71
- content = File.read("./setup.cfg")
93
+ content = File.read(path("setup.cfg"))
72
94
  name = content[/^name\s*=\s*(.+)/, 1]&.strip
73
95
  license = content[/^license\s*=\s*(.+)/, 1]&.strip
74
- { name: name, license: license }
96
+ version = content[/^version\s*=\s*(.+)/, 1]&.strip
97
+ { name: name, license: license, version: version }
75
98
  end
76
99
  end
77
100
 
78
101
  def pyproject_toml_data
79
- return unless File.exist?("./pyproject.toml")
102
+ return unless File.exist?(path("pyproject.toml"))
80
103
 
81
104
  @pyproject_toml_data ||= begin
82
105
  require "tomlrb"
83
- data = Tomlrb.load_file("./pyproject.toml")
106
+ data = Tomlrb.load_file(path("pyproject.toml"))
84
107
  project = data["project"] || data.dig("tool", "poetry") || {}
85
108
  license = project["license"]
86
109
  license = license["text"] if license.is_a?(Hash)
87
- { name: project["name"], license: license }
110
+ { name: project["name"], license: license, version: project["version"] }
88
111
  rescue LoadError
89
112
  nil
90
113
  end
91
114
  end
92
115
 
93
116
  def go_mod_data
94
- return unless File.exist?("./go.mod")
117
+ return unless File.exist?(path("go.mod"))
95
118
 
96
119
  @go_mod_data ||= begin
97
- name = File.read("./go.mod")[/^module\s+(\S+)/, 1]
120
+ name = File.read(path("go.mod"))[/^module\s+(\S+)/, 1]
98
121
  { name: name }
99
122
  end
100
123
  end
101
124
 
102
125
  def godeps_data
103
- return unless File.exist?("./Godeps/Godeps.json")
126
+ return unless File.exist?(path("Godeps/Godeps.json"))
104
127
 
105
128
  @godeps_data ||= begin
106
- data = JSON.parse(File.read("./Godeps/Godeps.json"))
129
+ data = JSON.parse(File.read(path("Godeps/Godeps.json")))
107
130
  { name: data["ImportPath"] }
108
131
  rescue JSON::ParserError
109
132
  nil
@@ -111,18 +134,22 @@ module LicenseConflicts
111
134
  end
112
135
 
113
136
  def pom_xml_data
114
- return unless File.exist?("./pom.xml")
137
+ return unless File.exist?(path("pom.xml"))
115
138
 
116
139
  @pom_xml_data ||= begin
117
140
  require "rexml/document"
118
- doc = REXML::Document.new(File.read("./pom.xml"))
119
- name = REXML::XPath.first(doc, "//project/name")&.text ||
120
- REXML::XPath.first(doc, "//project/artifactId")&.text
121
- license = REXML::XPath.first(doc, "//project/licenses/license/name")&.text
122
- { name: name, license: license }
141
+ doc = REXML::Document.new(File.read(path("pom.xml")))
142
+ name = xml_text(doc, "//project/name") || xml_text(doc, "//project/artifactId")
143
+ { name: name, license: xml_text(doc, "//project/licenses/license/name"),
144
+ version: xml_text(doc, "//project/version") }
123
145
  rescue REXML::ParseException
124
146
  nil
125
147
  end
126
148
  end
149
+
150
+ def xml_text(doc, xpath)
151
+ REXML::XPath.first(doc, xpath)&.text
152
+ end
127
153
  end
154
+ # rubocop:enable Metrics/ClassLength
128
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LicenseConflicts
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -4,6 +4,7 @@ require_relative 'license_conflicts/finder'
4
4
  require_relative 'license_conflicts/version'
5
5
  require_relative 'license_conflicts/report'
6
6
  require_relative 'license_conflicts/project_metadata'
7
+ require_relative "license_conflicts/declared_dependencies"
7
8
  require_relative 'license_conflicts/license_normalizer'
8
9
 
9
10
  module LicenseConflicts
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: license_conflicts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dennis Urtubia
@@ -132,6 +132,7 @@ files:
132
132
  - lib/license_conflicts.rb
133
133
  - lib/license_conflicts/cli.rb
134
134
  - lib/license_conflicts/conflicts_map.rb
135
+ - lib/license_conflicts/declared_dependencies.rb
135
136
  - lib/license_conflicts/finder.rb
136
137
  - lib/license_conflicts/license_normalizer.rb
137
138
  - lib/license_conflicts/project_metadata.rb