Skip to content

Commit

Permalink
Merge pull request #2849 from Zastai/msbuild-sdk-references
Browse files Browse the repository at this point in the history
  • Loading branch information
xlgmokha authored Nov 16, 2021
2 parents 3380365 + 943dc7c commit aab1321
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 2 deletions.
63 changes: 61 additions & 2 deletions nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ProjectFileParser
"ItemGroup > Dependency, "\
"ItemGroup > DevelopmentDependency"

PROJECT_SDK_REGEX = %r{^([^/]+)/(\d+(?:[.]\d+(?:[.]\d+)?)?(?:[+-].*)?)$}.freeze
PROPERTY_REGEX = /\$\((?<property>.*?)\)/.freeze
ITEM_REGEX = /\@\((?<property>.*?)\)/.freeze

Expand All @@ -32,16 +33,19 @@ def dependency_set(project_file:)

doc = Nokogiri::XML(project_file.content)
doc.remove_namespaces!
# Look for regular package references
doc.css(DEPENDENCY_SELECTOR).each do |dependency_node|
name = dependency_name(dependency_node, project_file)
req = dependency_requirement(dependency_node, project_file)
version = dependency_version(dependency_node, project_file)
prop_name = req_property_name(dependency_node)

dependency =
build_dependency(name, req, version, prop_name, project_file)
dependency = build_dependency(name, req, version, prop_name, project_file)
dependency_set << dependency if dependency
end
# Look for SDK references; see:
# https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk
add_sdk_references(doc, dependency_set, project_file)

dependency_set
end
Expand All @@ -50,6 +54,61 @@ def dependency_set(project_file:)

attr_reader :dependency_files

def add_sdk_references(doc, dependency_set, project_file)
# These come in 3 flavours:
# - <Project Sdk="Name/Version">
# - <Sdk Name="Name" Version="Version" />
# - <Import Project="..." Sdk="Name" Version="Version" />
# None of these support the use of properties, nor do they allow child
# elements instead of attributes.
add_sdk_refs_from_project(doc, dependency_set, project_file)
add_sdk_refs_from_sdk_tags(doc, dependency_set, project_file)
add_sdk_refs_from_import_tags(doc, dependency_set, project_file)
end

def add_sdk_ref_from_project(sdk_references, dependency_set, project_file)
sdk_references.split(";")&.each do |sdk_reference|
m = sdk_reference.match(PROJECT_SDK_REGEX)
if m
dependency = build_dependency(m[1], m[2], m[2], nil, project_file)
dependency_set << dependency if dependency
end
end
end

def add_sdk_refs_from_import_tags(doc, dependency_set, project_file)
doc.xpath("/Project/Import").each do |import_node|
next unless import_node.attribute("Sdk") && import_node.attribute("Version")

name = import_node.attribute("Sdk")&.value&.strip
version = import_node.attribute("Version")&.value&.strip

dependency = build_dependency(name, version, version, nil, project_file)
dependency_set << dependency if dependency
end
end

def add_sdk_refs_from_project(doc, dependency_set, project_file)
doc.xpath("/Project").each do |project_node|
sdk_references = project_node.attribute("Sdk")&.value&.strip
next unless sdk_references

add_sdk_ref_from_project(sdk_references, dependency_set, project_file)
end
end

def add_sdk_refs_from_sdk_tags(doc, dependency_set, project_file)
doc.xpath("/Project/Sdk").each do |sdk_node|
next unless sdk_node.attribute("Version")

name = sdk_node.attribute("Name")&.value&.strip
version = sdk_node.attribute("Version")&.value&.strip

dependency = build_dependency(name, version, version, nil, project_file)
dependency_set << dependency if dependency
end
end

def build_dependency(name, req, version, prop_name, project_file)
return unless name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ class ProjectFileDeclarationFinder
<DevelopmentDependency [^>]*?/>|
<DevelopmentDependency [^>]*?[^/]>.*?</DevelopmentDependency>
}mx.freeze
SDK_IMPORT_REGEX =
/ <Import [^>]*?Sdk="[^"]*?"[^>]*?Version="[^"]*?"[^>]*?>
| <Import [^>]*?Version="[^"]*?"[^>]*?Sdk="[^"]*?"[^>]*?>
/mx.freeze
SDK_PROJECT_REGEX =
/ <Project [^>]*?Sdk="[^"]*?"[^>]*?>
/mx.freeze
SDK_SDK_REGEX =
/ <Sdk [^>]*?Name="[^"]*?"[^>]*?Version="[^"]*?"[^>]*?>
| <Sdk [^>]*?Version="[^"]*?"[^>]*?Name="[^"]*?"[^>]*?>
/mx.freeze

attr_reader :dependency_name, :declaring_requirement,
:dependency_files
Expand All @@ -33,6 +44,7 @@ def initialize(dependency_name:, dependency_files:,

def declaration_strings
@declaration_strings ||= fetch_declaration_strings
@declaration_strings += fetch_sdk_strings
end

def declaration_nodes
Expand Down Expand Up @@ -72,6 +84,10 @@ def fetch_declaration_strings
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity

def fetch_sdk_strings
sdk_project_strings + sdk_sdk_strings + sdk_import_strings
end

# rubocop:disable Metrics/PerceivedComplexity
def get_node_version_value(node)
attribute = "Version"
Expand All @@ -95,6 +111,71 @@ def declaring_file

raise "No file found with name #{filename}!"
end

def sdk_import_strings
sdk_strings(SDK_IMPORT_REGEX, "Import", "Sdk", "Version")
end

def parse_element(string, name)
xml = string
xml += "</#{name}>" unless string.end_with?("/>")
node = Nokogiri::XML(xml)
node.remove_namespaces!
node.at_xpath("/#{name}")
end

def get_attribute_value_nocase(element, name)
value = element.attribute(name)&.value ||
element.attribute(name.downcase)&.value ||
element.attribute(name.upcase)&.value
value&.strip
end

def desired_sdk_reference?(sdk_reference, dep_name, dep_version)
parts = sdk_reference.split("/")
parts.length == 2 && parts[0]&.downcase == dep_name && parts[1] == dep_version
end

def sdk_project_strings
dep_name = dependency_name&.downcase
dep_version = declaring_requirement.fetch(:requirement)
strings = []
declaring_file.content.scan(SDK_PROJECT_REGEX).each do |string|
element = parse_element(string, "Project")
next unless element

sdk_references = get_attribute_value_nocase(element, "Sdk")
next unless sdk_references&.include?("/")

sdk_references.split(";").each do |sdk_reference|
strings << sdk_reference if desired_sdk_reference?(sdk_reference, dep_name, dep_version)
end
end
strings.uniq
end

def sdk_sdk_strings
sdk_strings(SDK_SDK_REGEX, "Sdk", "Name", "Version")
end

def sdk_strings(regex, element_name, name_attribute, version_attribute)
dep_name = dependency_name&.downcase
dep_version = declaring_requirement.fetch(:requirement)
strings = []
declaring_file.content.scan(regex).each do |string|
element = parse_element(string, element_name)
next unless element

node_name = get_attribute_value_nocase(element, name_attribute)&.downcase
next unless node_name == dep_name

node_version = get_attribute_value_nocase(element, version_attribute)
next unless node_version == dep_version

strings << string
end
strings
end
end
end
end
Expand Down
116 changes: 116 additions & 0 deletions nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,122 @@
expect(dependencies.count).to eq(0)
end
end

context "with a versioned sdk reference" do
context "specified in the Project tag" do
let(:file_body) { fixture("csproj", "sdk_reference_via_project.csproj") }

its(:length) { is_expected.to eq(2) }

describe "the first dependency" do
subject(:dependency) { dependencies.first }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Awesome.Sdk")
expect(dependency.version).to eq("1.2.3")
expect(dependency.requirements).to eq([{
requirement: "1.2.3",
file: "my.csproj",
groups: [],
source: nil
}])
end
end

describe "the second dependency" do
subject(:dependency) { dependencies[1] }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Prototype.Sdk")
expect(dependency.version).to eq("0.1.0-beta")
expect(dependency.requirements).to eq([{
requirement: "0.1.0-beta",
file: "my.csproj",
groups: [],
source: nil
}])
end
end
end

context "specified via an Sdk tag" do
let(:file_body) { fixture("csproj", "sdk_reference_via_sdk.csproj") }

its(:length) { is_expected.to eq(2) }

describe "the first dependency" do
subject(:dependency) { dependencies.first }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Awesome.Sdk")
expect(dependency.version).to eq("1.2.3")
expect(dependency.requirements).to eq([{
requirement: "1.2.3",
file: "my.csproj",
groups: [],
source: nil
}])
end
end

describe "the second dependency" do
subject(:dependency) { dependencies[1] }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Prototype.Sdk")
expect(dependency.version).to eq("0.1.0-beta")
expect(dependency.requirements).to eq([{
requirement: "0.1.0-beta",
file: "my.csproj",
groups: [],
source: nil
}])
end
end
end

context "specified via an Import tag" do
let(:file_body) { fixture("csproj", "sdk_reference_via_import.csproj") }

its(:length) { is_expected.to eq(2) }

describe "the first dependency" do
subject(:dependency) { dependencies.first }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Awesome.Sdk")
expect(dependency.version).to eq("1.2.3")
expect(dependency.requirements).to eq([{
requirement: "1.2.3",
file: "my.csproj",
groups: [],
source: nil
}])
end
end

describe "the second dependency" do
subject(:dependency) { dependencies[1] }

it "has the right details" do
expect(dependency).to be_a(Dependabot::Dependency)
expect(dependency.name).to eq("Prototype.Sdk")
expect(dependency.version).to eq("0.1.0-beta")
expect(dependency.requirements).to eq([{
requirement: "0.1.0-beta",
file: "my.csproj",
groups: [],
source: nil
}])
end
end
end
end
end
end
end
43 changes: 43 additions & 0 deletions nuget/spec/dependabot/nuget/file_updater_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,49 @@
)
end
end

context "with MSBuild SDKs" do
let(:csproj_body) do
fixture("csproj", "sdk_references_of_all_kinds.csproj")
end
let(:dependency_name) { "Foo.Bar" }
let(:version) { "1.2.3" }
let(:previous_version) { "1.1.1" }
let(:requirements) do
[{
requirement: "1.2.3",
file: "my.csproj",
groups: [],
source: nil
}]
end
let(:previous_requirements) do
[{
requirement: "1.1.1",
file: "my.csproj",
groups: [],
source: nil
}]
end

it "updates the project correctly" do
content = updated_csproj_file.content
# Sdk attribute on Project (front, middle, back)
expect(content).to include(%(Sdk="Foo.Bar/1.2.3;))
expect(content).to include(%(X;Foo.Bar/1.2.3;Y))
expect(content).to include(%(Y;Foo.Bar/1.2.3">))
# Sdk tag (name/version and version/name)
expect(content).to include(%(<Sdk Version="1.2.3" Name="Foo.Bar"))
expect(content).to include(%(<Sdk Name="Foo.Bar" Version="1.2.3"))
# Import tag (name/version and version/name)
expect(content).to include(
%(<Import Project="X" Version="1.2.3" Sdk="Foo.Bar")
)
expect(content).to include(
%(<Import Sdk="Foo.Bar" Project="Y" Version="1.2.3")
)
end
end
end

context "with a packages.config file" do
Expand Down
11 changes: 11 additions & 0 deletions nuget/spec/fixtures/csproj/sdk_reference_via_import.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project>

<Import Project="Awesome.props" Sdk="Awesome.Sdk" Version="1.2.3" />

<PropertyGroup>
<Description>Very simple project using a custom SDK.</Description>
</PropertyGroup>

<Import Project="Prototype.targets" Sdk="Prototype.Sdk" Version="0.1.0-beta" />

</Project>
7 changes: 7 additions & 0 deletions nuget/spec/fixtures/csproj/sdk_reference_via_project.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk;Awesome.Sdk/1.2.3;Prototype.Sdk/0.1.0-beta">

<PropertyGroup>
<Description>Very simple project using a custom SDK.</Description>
</PropertyGroup>

</Project>
Loading

0 comments on commit aab1321

Please sign in to comment.