dependabot-nuget 0.80.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 +7 -0
- data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +49 -0
- data/lib/dependabot/nuget/file_fetcher/sln_project_paths_finder.rb +53 -0
- data/lib/dependabot/nuget/file_fetcher.rb +216 -0
- data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +63 -0
- data/lib/dependabot/nuget/file_parser/project_file_parser.rb +154 -0
- data/lib/dependabot/nuget/file_parser/property_value_finder.rb +129 -0
- data/lib/dependabot/nuget/file_parser.rb +86 -0
- data/lib/dependabot/nuget/file_updater/packages_config_declaration_finder.rb +67 -0
- data/lib/dependabot/nuget/file_updater/project_file_declaration_finder.rb +76 -0
- data/lib/dependabot/nuget/file_updater/property_value_updater.rb +62 -0
- data/lib/dependabot/nuget/file_updater.rb +152 -0
- data/lib/dependabot/nuget/metadata_finder.rb +117 -0
- data/lib/dependabot/nuget/requirement.rb +90 -0
- data/lib/dependabot/nuget/update_checker/property_updater.rb +95 -0
- data/lib/dependabot/nuget/update_checker/repository_finder.rb +230 -0
- data/lib/dependabot/nuget/update_checker/requirements_updater.rb +79 -0
- data/lib/dependabot/nuget/update_checker/version_finder.rb +229 -0
- data/lib/dependabot/nuget/update_checker.rb +128 -0
- data/lib/dependabot/nuget/version.rb +23 -0
- data/lib/dependabot/nuget.rb +10 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 07c225b6dcae09a617444df10ca1f5fc0aaf1ae159ba376d980c84d26a758f39
|
4
|
+
data.tar.gz: 7a304f35e363cc2a6f40b7c040bba03b14b848add5e5048e9bdbab8ae68c2aab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da29d11282b5b11e47a33066c43172b82386203939acc7241496a05ca2b97a91a2aedab4c71123fe006e11bd0f745f14f52e2de5abf00164205917c34f401672
|
7
|
+
data.tar.gz: a5996018d925129d2940ee06c7b7382885d13b0ec6fb95b82c4d09fb0bd4987a8af97d5575d86892a8b59687ab25419015d55fc4d55b12b7766f8f656421a131
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
require "dependabot/nuget/file_fetcher"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Nuget
|
10
|
+
class FileFetcher
|
11
|
+
class ImportPathsFinder
|
12
|
+
def initialize(project_file:)
|
13
|
+
@project_file = project_file
|
14
|
+
end
|
15
|
+
|
16
|
+
def import_paths
|
17
|
+
doc = Nokogiri::XML(project_file.content)
|
18
|
+
doc.remove_namespaces!
|
19
|
+
doc.xpath("/Project/Import").map do |import_node|
|
20
|
+
path = import_node.attribute("Project").value.strip.tr("\\", "/")
|
21
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
22
|
+
Pathname.new(path).cleanpath.to_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def project_reference_paths
|
27
|
+
doc = Nokogiri::XML(project_file.content)
|
28
|
+
doc.remove_namespaces!
|
29
|
+
doc.xpath("/Project/ItemGroup/ProjectReference").map do |node|
|
30
|
+
path = node.attribute("Include").value.strip.tr("\\", "/")
|
31
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
32
|
+
Pathname.new(path).cleanpath.to_path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :project_file
|
39
|
+
|
40
|
+
def current_dir
|
41
|
+
parts = project_file.name.split("/")[0..-2]
|
42
|
+
return if parts.empty?
|
43
|
+
|
44
|
+
parts.join("/")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "dependabot/nuget/file_fetcher"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Nuget
|
8
|
+
class FileFetcher
|
9
|
+
class SlnProjectPathsFinder
|
10
|
+
PROJECT_PATH_REGEX =
|
11
|
+
/(?<=["'])[^"']*?\.(?:vb|cs|fs)proj(?=["'])/.freeze
|
12
|
+
|
13
|
+
def initialize(sln_file:)
|
14
|
+
@sln_file = sln_file
|
15
|
+
end
|
16
|
+
|
17
|
+
def project_paths
|
18
|
+
paths = []
|
19
|
+
sln_file_lines = sln_file.content.lines
|
20
|
+
|
21
|
+
sln_file_lines.each_with_index do |line, index|
|
22
|
+
next unless line.match?(/^\s*Project/)
|
23
|
+
|
24
|
+
# Don't know how to handle multi-line project declarations yet
|
25
|
+
next unless sln_file_lines[index + 1]&.match?(/^\s*EndProject/)
|
26
|
+
|
27
|
+
path = line.split('"')[5]
|
28
|
+
path = path.tr("\\", "/")
|
29
|
+
|
30
|
+
# If the path doesn't have an extension it's probably a directory
|
31
|
+
next unless path.match?(/\.[a-z]{2}proj$/)
|
32
|
+
|
33
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
34
|
+
paths << Pathname.new(path).cleanpath.to_path
|
35
|
+
end
|
36
|
+
|
37
|
+
paths
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :sln_file
|
43
|
+
|
44
|
+
def current_dir
|
45
|
+
parts = sln_file.name.split("/")[0..-2]
|
46
|
+
return if parts.empty?
|
47
|
+
|
48
|
+
parts.join("/")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/file_fetchers"
|
4
|
+
require "dependabot/file_fetchers/base"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Nuget
|
8
|
+
class FileFetcher < Dependabot::FileFetchers::Base
|
9
|
+
require_relative "file_fetcher/import_paths_finder"
|
10
|
+
require_relative "file_fetcher/sln_project_paths_finder"
|
11
|
+
|
12
|
+
def self.required_files_in?(filenames)
|
13
|
+
return true if filenames.any? { |f| f.match?(/^packages\.config$/i) }
|
14
|
+
return true if filenames.any? { |f| f.end_with?(".sln") }
|
15
|
+
|
16
|
+
filenames.any? { |name| name.match?(%r{^[^/]*\.[a-z]{2}proj$}) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.required_files_message
|
20
|
+
"Repo must contain a .(cs|vb|fs)proj file or a packages.config."
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def fetch_files
|
26
|
+
fetched_files = []
|
27
|
+
fetched_files += project_files
|
28
|
+
fetched_files += directory_build_props_files
|
29
|
+
fetched_files += imported_property_files
|
30
|
+
|
31
|
+
fetched_files += packages_config_files
|
32
|
+
fetched_files << nuget_config if nuget_config
|
33
|
+
|
34
|
+
fetched_files = fetched_files.uniq
|
35
|
+
|
36
|
+
if project_files.none? && packages_config_files.none?
|
37
|
+
raise(
|
38
|
+
Dependabot::DependencyFileNotFound,
|
39
|
+
File.join(directory, "<anything>.(cs|vb|fs)proj")
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
fetched_files
|
44
|
+
end
|
45
|
+
|
46
|
+
def project_files
|
47
|
+
@project_files ||=
|
48
|
+
begin
|
49
|
+
project_files = []
|
50
|
+
project_files << csproj_file if csproj_file
|
51
|
+
project_files << vbproj_file if vbproj_file
|
52
|
+
project_files << fsproj_file if fsproj_file
|
53
|
+
|
54
|
+
project_files += sln_project_files
|
55
|
+
project_files
|
56
|
+
end
|
57
|
+
rescue Octokit::NotFound, Gitlab::Error::NotFound
|
58
|
+
raise(
|
59
|
+
Dependabot::DependencyFileNotFound,
|
60
|
+
File.join(directory, "<anything>.(cs|vb|fs)proj")
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def packages_config_files
|
65
|
+
return @packages_config_files if @packages_config_files
|
66
|
+
|
67
|
+
candidate_paths =
|
68
|
+
[*project_files.map { |f| File.dirname(f.name) }, "."].uniq
|
69
|
+
|
70
|
+
@packages_config_files ||=
|
71
|
+
candidate_paths.map do |dir|
|
72
|
+
file = repo_contents(dir: dir).
|
73
|
+
find { |f| f.name.casecmp("packages.config").zero? }
|
74
|
+
fetch_file_from_host(File.join(dir, file.name)) if file
|
75
|
+
end.compact
|
76
|
+
end
|
77
|
+
|
78
|
+
def sln_file
|
79
|
+
return unless sln_file_name
|
80
|
+
|
81
|
+
@sln_file ||= fetch_file_from_host(sln_file_name)
|
82
|
+
end
|
83
|
+
|
84
|
+
def sln_file_name
|
85
|
+
sln_files = repo_contents.select { |f| f.name.end_with?(".sln") }
|
86
|
+
|
87
|
+
# If there are no sln files, just return `nil`
|
88
|
+
return if sln_files.none?
|
89
|
+
|
90
|
+
# Use the biggest sln file
|
91
|
+
sln_files.max_by(&:size).name
|
92
|
+
end
|
93
|
+
|
94
|
+
def directory_build_props_files
|
95
|
+
return @directory_build_props_files if @directory_build_checked
|
96
|
+
|
97
|
+
@directory_build_checked = true
|
98
|
+
attempted_paths = []
|
99
|
+
@directory_build_props_files = []
|
100
|
+
|
101
|
+
# Don't need to insert "." here, because Directory.Build.props files
|
102
|
+
# can only be used by project files (not packages.config ones)
|
103
|
+
project_files.map { |f| File.dirname(f.name) }.uniq.map do |dir|
|
104
|
+
possible_paths = dir.split("/").map.with_index do |_, i|
|
105
|
+
base = dir.split("/").first(i + 1).join("/")
|
106
|
+
Pathname.new(base + "/Directory.Build.props").cleanpath.to_path
|
107
|
+
end.reverse + ["Directory.Build.props"]
|
108
|
+
|
109
|
+
possible_paths.each do |path|
|
110
|
+
break if attempted_paths.include?(path)
|
111
|
+
|
112
|
+
attempted_paths << path
|
113
|
+
@directory_build_props_files << fetch_file_from_host(path)
|
114
|
+
rescue Dependabot::DependencyFileNotFound
|
115
|
+
next
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
@directory_build_props_files
|
120
|
+
end
|
121
|
+
|
122
|
+
def sln_project_files
|
123
|
+
return [] unless sln_file
|
124
|
+
|
125
|
+
@sln_project_files ||=
|
126
|
+
begin
|
127
|
+
paths = SlnProjectPathsFinder.
|
128
|
+
new(sln_file: sln_file).
|
129
|
+
project_paths
|
130
|
+
|
131
|
+
paths.map do |path|
|
132
|
+
fetch_file_from_host(path)
|
133
|
+
rescue Dependabot::DependencyFileNotFound
|
134
|
+
# Don't worry about missing files too much for now (at least
|
135
|
+
# until we start resolving properties)
|
136
|
+
nil
|
137
|
+
end.compact
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def csproj_file
|
142
|
+
@csproj_file ||=
|
143
|
+
begin
|
144
|
+
file = repo_contents.find { |f| f.name.end_with?(".csproj") }
|
145
|
+
fetch_file_from_host(file.name) if file
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def vbproj_file
|
150
|
+
@vbproj_file ||=
|
151
|
+
begin
|
152
|
+
file = repo_contents.find { |f| f.name.end_with?(".vbproj") }
|
153
|
+
fetch_file_from_host(file.name) if file
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def fsproj_file
|
158
|
+
@fsproj_file ||=
|
159
|
+
begin
|
160
|
+
file = repo_contents.find { |f| f.name.end_with?(".fsproj") }
|
161
|
+
fetch_file_from_host(file.name) if file
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def nuget_config
|
166
|
+
@nuget_config ||=
|
167
|
+
begin
|
168
|
+
file = repo_contents.
|
169
|
+
find { |f| f.name.casecmp("nuget.config").zero? }
|
170
|
+
file = fetch_file_from_host(file.name) if file
|
171
|
+
file&.tap { |f| f.support_file = true }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def imported_property_files
|
176
|
+
imported_property_files = []
|
177
|
+
|
178
|
+
[*project_files, *directory_build_props_files].each do |proj_file|
|
179
|
+
previously_fetched_files = project_files + imported_property_files
|
180
|
+
imported_property_files +=
|
181
|
+
fetch_imported_property_files(
|
182
|
+
file: proj_file,
|
183
|
+
previously_fetched_files: previously_fetched_files
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
imported_property_files
|
188
|
+
end
|
189
|
+
|
190
|
+
def fetch_imported_property_files(file:, previously_fetched_files:)
|
191
|
+
paths =
|
192
|
+
ImportPathsFinder.new(project_file: file).import_paths +
|
193
|
+
ImportPathsFinder.new(project_file: file).project_reference_paths
|
194
|
+
|
195
|
+
paths.flat_map do |path|
|
196
|
+
next if previously_fetched_files.map(&:name).include?(path)
|
197
|
+
next if file.name == path
|
198
|
+
next if path.include?("$(")
|
199
|
+
|
200
|
+
fetched_file = fetch_file_from_host(path)
|
201
|
+
grandchild_property_files = fetch_imported_property_files(
|
202
|
+
file: fetched_file,
|
203
|
+
previously_fetched_files: previously_fetched_files + [file]
|
204
|
+
)
|
205
|
+
[fetched_file, *grandchild_property_files]
|
206
|
+
rescue Dependabot::DependencyFileNotFound
|
207
|
+
# Don't worry about missing files too much for now (at least
|
208
|
+
# until we start resolving properties)
|
209
|
+
nil
|
210
|
+
end.compact
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
Dependabot::FileFetchers.register("nuget", Dependabot::Nuget::FileFetcher)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/nuget/file_parser"
|
7
|
+
|
8
|
+
# For details on packages.config files see:
|
9
|
+
# https://docs.microsoft.com/en-us/nuget/reference/packages-config
|
10
|
+
module Dependabot
|
11
|
+
module Nuget
|
12
|
+
class FileParser
|
13
|
+
class PackagesConfigParser
|
14
|
+
require "dependabot/file_parsers/base/dependency_set"
|
15
|
+
|
16
|
+
DEPENDENCY_SELECTOR = "packages > package"
|
17
|
+
|
18
|
+
def initialize(packages_config:)
|
19
|
+
@packages_config = packages_config
|
20
|
+
end
|
21
|
+
|
22
|
+
def dependency_set
|
23
|
+
dependency_set = Dependabot::FileParsers::Base::DependencySet.new
|
24
|
+
|
25
|
+
doc = Nokogiri::XML(packages_config.content)
|
26
|
+
doc.remove_namespaces!
|
27
|
+
doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
|
28
|
+
dependency_set <<
|
29
|
+
Dependency.new(
|
30
|
+
name: dependency_name(dependency_node),
|
31
|
+
version: dependency_version(dependency_node),
|
32
|
+
package_manager: "nuget",
|
33
|
+
requirements: [{
|
34
|
+
requirement: dependency_version(dependency_node),
|
35
|
+
file: packages_config.name,
|
36
|
+
groups: [],
|
37
|
+
source: nil
|
38
|
+
}]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
dependency_set
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :packages_config
|
48
|
+
|
49
|
+
def dependency_name(dependency_node)
|
50
|
+
dependency_node.attribute("id")&.value&.strip ||
|
51
|
+
dependency_node.at_xpath("./id")&.content&.strip
|
52
|
+
end
|
53
|
+
|
54
|
+
def dependency_version(dependency_node)
|
55
|
+
# Ranges and wildcards aren't allowed in a packages.config - the
|
56
|
+
# specified requirement is always an exact version.
|
57
|
+
dependency_node.attribute("version")&.value&.strip ||
|
58
|
+
dependency_node.at_xpath("./version")&.content&.strip
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/nuget/file_parser"
|
7
|
+
|
8
|
+
# For details on how dotnet handles version constraints, see:
|
9
|
+
# https://docs.microsoft.com/en-us/nuget/reference/package-versioning
|
10
|
+
module Dependabot
|
11
|
+
module Nuget
|
12
|
+
class FileParser
|
13
|
+
class ProjectFileParser
|
14
|
+
require "dependabot/file_parsers/base/dependency_set"
|
15
|
+
require_relative "property_value_finder"
|
16
|
+
|
17
|
+
DEPENDENCY_SELECTOR = "ItemGroup > PackageReference, "\
|
18
|
+
"ItemGroup > Dependency, "\
|
19
|
+
"ItemGroup > DevelopmentDependency"
|
20
|
+
|
21
|
+
PROPERTY_REGEX = /\$\((?<property>.*?)\)/.freeze
|
22
|
+
|
23
|
+
def initialize(dependency_files:)
|
24
|
+
@dependency_files = dependency_files
|
25
|
+
end
|
26
|
+
|
27
|
+
def dependency_set(project_file:)
|
28
|
+
dependency_set = Dependabot::FileParsers::Base::DependencySet.new
|
29
|
+
|
30
|
+
doc = Nokogiri::XML(project_file.content)
|
31
|
+
doc.remove_namespaces!
|
32
|
+
doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
|
33
|
+
name = dependency_name(dependency_node, project_file)
|
34
|
+
req = dependency_requirement(dependency_node, project_file)
|
35
|
+
version = dependency_version(dependency_node, project_file)
|
36
|
+
prop_name = req_property_name(dependency_node)
|
37
|
+
|
38
|
+
dependency =
|
39
|
+
build_dependency(name, req, version, prop_name, project_file)
|
40
|
+
dependency_set << dependency if dependency
|
41
|
+
end
|
42
|
+
|
43
|
+
dependency_set
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :dependency_files
|
49
|
+
|
50
|
+
def build_dependency(name, req, version, prop_name, project_file)
|
51
|
+
return unless name
|
52
|
+
|
53
|
+
# Exclude any dependencies specified using interpolation
|
54
|
+
return if [name, req, version].any? { |s| s&.include?("%(") }
|
55
|
+
|
56
|
+
requirement = {
|
57
|
+
requirement: req,
|
58
|
+
file: project_file.name,
|
59
|
+
groups: [],
|
60
|
+
source: nil
|
61
|
+
}
|
62
|
+
|
63
|
+
if prop_name
|
64
|
+
# Get the root property name unless no details could be found,
|
65
|
+
# in which case use the top-level name to ease debugging
|
66
|
+
root_prop_name = details_for_property(prop_name, project_file)&.
|
67
|
+
fetch(:root_property_name) || prop_name
|
68
|
+
requirement[:metadata] = { property_name: root_prop_name }
|
69
|
+
end
|
70
|
+
|
71
|
+
Dependency.new(
|
72
|
+
name: name,
|
73
|
+
version: version,
|
74
|
+
package_manager: "nuget",
|
75
|
+
requirements: [requirement]
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def dependency_name(dependency_node, project_file)
|
80
|
+
raw_name =
|
81
|
+
dependency_node.attribute("Include")&.value&.strip ||
|
82
|
+
dependency_node.at_xpath("./Include")&.content&.strip
|
83
|
+
return unless raw_name
|
84
|
+
|
85
|
+
evaluated_value(raw_name, project_file)
|
86
|
+
end
|
87
|
+
|
88
|
+
def dependency_requirement(dependency_node, project_file)
|
89
|
+
raw_requirement =
|
90
|
+
dependency_node.attribute("Version")&.value&.strip ||
|
91
|
+
dependency_node.at_xpath("./Version")&.content&.strip
|
92
|
+
return unless raw_requirement
|
93
|
+
|
94
|
+
evaluated_value(raw_requirement, project_file)
|
95
|
+
end
|
96
|
+
|
97
|
+
def dependency_version(dependency_node, project_file)
|
98
|
+
requirement = dependency_requirement(dependency_node, project_file)
|
99
|
+
return unless requirement
|
100
|
+
|
101
|
+
# Remove brackets if present
|
102
|
+
version = requirement.gsub(/[\(\)\[\]]/, "").strip
|
103
|
+
|
104
|
+
# We don't know the version for range requirements or wildcard
|
105
|
+
# requirements, so return `nil` for these.
|
106
|
+
return if version.include?(",") || version.include?("*") ||
|
107
|
+
version == ""
|
108
|
+
|
109
|
+
version
|
110
|
+
end
|
111
|
+
|
112
|
+
def req_property_name(dependency_node)
|
113
|
+
raw_requirement =
|
114
|
+
dependency_node.attribute("Version")&.value&.strip ||
|
115
|
+
dependency_node.at_xpath("./Version")&.content&.strip
|
116
|
+
return unless raw_requirement
|
117
|
+
|
118
|
+
return unless raw_requirement.match?(PROPERTY_REGEX)
|
119
|
+
|
120
|
+
raw_requirement.
|
121
|
+
match(PROPERTY_REGEX).
|
122
|
+
named_captures.fetch("property")
|
123
|
+
end
|
124
|
+
|
125
|
+
def evaluated_value(value, project_file)
|
126
|
+
return value unless value.match?(PROPERTY_REGEX)
|
127
|
+
|
128
|
+
property_name = value.match(PROPERTY_REGEX).
|
129
|
+
named_captures.fetch("property")
|
130
|
+
property_details = details_for_property(property_name, project_file)
|
131
|
+
|
132
|
+
# Don't halt parsing for a missing property value until we're
|
133
|
+
# confident we're fetching property values correctly
|
134
|
+
return value unless property_details&.fetch(:value)
|
135
|
+
|
136
|
+
value.gsub(PROPERTY_REGEX, property_details&.fetch(:value))
|
137
|
+
end
|
138
|
+
|
139
|
+
def details_for_property(property_name, project_file)
|
140
|
+
property_value_finder.
|
141
|
+
property_details(
|
142
|
+
property_name: property_name,
|
143
|
+
callsite_file: project_file
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
def property_value_finder
|
148
|
+
@property_value_finder ||=
|
149
|
+
PropertyValueFinder.new(dependency_files: dependency_files)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/nuget/file_fetcher/import_paths_finder"
|
4
|
+
require "dependabot/nuget/file_parser"
|
5
|
+
|
6
|
+
# For docs, see:
|
7
|
+
# - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-properties
|
8
|
+
# - https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build
|
9
|
+
module Dependabot
|
10
|
+
module Nuget
|
11
|
+
class FileParser
|
12
|
+
class PropertyValueFinder
|
13
|
+
PROPERTY_REGEX = /\$\((?<property>.*?)\)/.freeze
|
14
|
+
|
15
|
+
def initialize(dependency_files:)
|
16
|
+
@dependency_files = dependency_files
|
17
|
+
end
|
18
|
+
|
19
|
+
def property_details(property_name:, callsite_file:, stack: [])
|
20
|
+
stack += [[property_name, callsite_file.name]]
|
21
|
+
|
22
|
+
node_details = deep_find_prop_node(
|
23
|
+
property: property_name,
|
24
|
+
file: callsite_file
|
25
|
+
)
|
26
|
+
|
27
|
+
node_details ||=
|
28
|
+
find_property_in_directory_build_props(
|
29
|
+
property: property_name,
|
30
|
+
callsite_file: callsite_file
|
31
|
+
)
|
32
|
+
|
33
|
+
return unless node_details
|
34
|
+
return node_details unless node_details[:value] =~ PROPERTY_REGEX
|
35
|
+
|
36
|
+
check_next_level_of_stack(node_details, stack)
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_next_level_of_stack(node_details, stack)
|
40
|
+
property_name = node_details.fetch(:value).
|
41
|
+
match(PROPERTY_REGEX).
|
42
|
+
named_captures.fetch("property")
|
43
|
+
callsite_file = dependency_files.
|
44
|
+
find { |f| f.name == node_details.fetch(:file) }
|
45
|
+
|
46
|
+
if stack.include?([property_name, callsite_file.name])
|
47
|
+
raise "Circular reference!"
|
48
|
+
end
|
49
|
+
|
50
|
+
property_details(
|
51
|
+
property_name: property_name,
|
52
|
+
callsite_file: callsite_file,
|
53
|
+
stack: stack
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :dependency_files
|
60
|
+
|
61
|
+
def deep_find_prop_node(property:, file:)
|
62
|
+
doc = Nokogiri::XML(file.content)
|
63
|
+
doc.remove_namespaces!
|
64
|
+
node = doc.at_xpath(property_xpath(property))
|
65
|
+
|
66
|
+
# If we found a value for the property, return it
|
67
|
+
if node
|
68
|
+
return node_details(file: file, node: node, property: property)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Otherwise, we need to look in an imported file
|
72
|
+
import_path_finder =
|
73
|
+
Nuget::FileFetcher::ImportPathsFinder.
|
74
|
+
new(project_file: file)
|
75
|
+
|
76
|
+
import_paths = [
|
77
|
+
*import_path_finder.import_paths,
|
78
|
+
*import_path_finder.project_reference_paths
|
79
|
+
]
|
80
|
+
|
81
|
+
file = import_paths.
|
82
|
+
map { |p| dependency_files.find { |f| f.name == p } }.
|
83
|
+
compact.
|
84
|
+
find { |f| deep_find_prop_node(property: property, file: f) }
|
85
|
+
|
86
|
+
return unless file
|
87
|
+
|
88
|
+
deep_find_prop_node(property: property, file: file)
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_property_in_directory_build_props(property:, callsite_file:)
|
92
|
+
file = buildfile_for_project(callsite_file)
|
93
|
+
return unless file
|
94
|
+
|
95
|
+
deep_find_prop_node(property: property, file: file)
|
96
|
+
end
|
97
|
+
|
98
|
+
def buildfile_for_project(project_file)
|
99
|
+
dir = File.dirname(project_file.name)
|
100
|
+
|
101
|
+
# Nuget walks up the directory structure looking for a
|
102
|
+
# Directory.Build.props file
|
103
|
+
possible_paths = dir.split("/").map.with_index do |_, i|
|
104
|
+
base = dir.split("/").first(i + 1).join("/")
|
105
|
+
Pathname.new(base + "/Directory.Build.props").cleanpath.to_path
|
106
|
+
end.reverse + ["Directory.Build.props"]
|
107
|
+
|
108
|
+
path = possible_paths.uniq.
|
109
|
+
find { |p| dependency_files.find { |f| f.name == p } }
|
110
|
+
|
111
|
+
dependency_files.find { |f| f.name == path }
|
112
|
+
end
|
113
|
+
|
114
|
+
def property_xpath(property_name)
|
115
|
+
"/Project/PropertyGroup/#{property_name}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def node_details(file:, node:, property:)
|
119
|
+
{
|
120
|
+
file: file.name,
|
121
|
+
node: node,
|
122
|
+
value: node.content.strip,
|
123
|
+
root_property_name: property
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|