diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index 34dfd4b95b0..875f8014955 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -63,9 +63,18 @@ def parse def project_file_dependencies dependency_set = DependencySet.new - (project_files + project_import_files).each do |file| - parser = project_file_parser - dependency_set += parser.dependency_set(project_file: file) + project_files.each do |project_file| + tfms = project_file_parser.target_frameworks(project_file: project_file) + unless tfms.any? + Dependabot.logger.warn "Excluding project file '#{project_file.name}' due to unresolvable target framework" + next + end + + dependency_set += project_file_parser.dependency_set(project_file: project_file) + end + + proj_files.each do |proj_file| + dependency_set += project_file_parser.dependency_set(project_file: proj_file) end dependency_set @@ -109,14 +118,21 @@ def project_file_parser ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } + def proj_files + projfile = /\.proj$/ + + dependency_files.select do |df| + df.name.match?(projfile) + end + end + sig { returns(T::Array[Dependabot::DependencyFile]) } def project_files - projfile = /\.([a-z]{2})?proj$/ - packageprops = /[Dd]irectory.[Pp]ackages.props/ + projectfile = /\.(cs|vb|fs)proj$/ dependency_files.select do |df| - df.name.match?(projfile) || - df.name.match?(packageprops) + df.name.match?(projectfile) end end @@ -144,19 +160,24 @@ def nuget_configs sig { returns(T.nilable(Dependabot::DependencyFile)) } def global_json - dependency_files.find { |f| f.name.casecmp("global.json")&.zero? } + dependency_files.find { |f| f.name.casecmp?("global.json") } end sig { returns(T.nilable(Dependabot::DependencyFile)) } def dotnet_tools_json - dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json")&.zero? } + dependency_files.find { |f| f.name.casecmp?(".config/dotnet-tools.json") } end sig { override.void } def check_required_files - return if project_files.any? || packages_config_files.any? + if project_files.any? || proj_files.any? || packages_config_files.any? || global_json || dotnet_tools_json + return + end - raise "No project file or packages.config!" + raise Dependabot::DependencyFileNotFound.new( + "*.(cs|vb|fs)proj, *.proj, packages.config, global.json, dotnet-tools.json", + "No project file, *.proj, packages.config, global.json, or dotnet-tools.json!" + ) end end end diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index 37f0dc446b1..f20b4698d46 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -15,12 +15,13 @@ it_behaves_like "a dependency file parser" - let(:files) { [csproj_file] } + let(:files) { [csproj_file] + additional_files } + let(:additional_files) { [] } let(:csproj_file) do Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) end let(:csproj_body) { fixture("csproj", "basic.csproj") } - let(:parser) { described_class.new(dependency_files: files, source: source) } + let(:parser) { described_class.new(dependency_files: files, source: source, repo_contents_path: "/test/repo") } let(:source) do Dependabot::Source.new( provider: "github", @@ -29,53 +30,37 @@ ) end - def dependencies_from_info(deps_info) - deps = deps_info.map do |info| - Dependabot::Dependency.new( - name: info[:name], - version: info[:version], - requirements: [ - { - requirement: info[:version], - file: info[:file], - groups: ["dependencies"], - source: nil - } - ], - package_manager: "nuget" - ) - end - - Dependabot::FileParsers::Base::DependencySet.new(deps) - end - describe "parse" do let(:dependencies) { parser.parse } subject(:top_level_dependencies) { dependencies.select(&:top_level?) } - context "with a .proj file" do - let(:files) { [proj_file] } - let(:proj_file) do + context "with a dirs.proj file" do + let(:additional_files) { [dirs_proj_file] } + let(:csproj_file) do + # only .csproj, .vbproj, and .fsproj files are supported, but this is faked to ensure that the parser navigates + # through elements + Dependabot::DependencyFile.new(name: "my.not-csproj", content: csproj_body) + end + let(:csproj_body) { fixture("csproj", "basic2.csproj") } + let(:dirs_proj_file) do Dependabot::DependencyFile.new( - name: "proj.proj", - content: fixture("csproj", "basic2.csproj") + name: "dirs.proj", + content: + <<~XML + + + + + + XML ) end - let(:proj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.0.1", file: "proj.proj" }, - { name: "Serilog", version: "2.3.0", file: "proj.proj" } - ] - end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: proj_file).and_return( - dependencies_from_info(proj_dependencies) - ) + stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1"]) + stub_search_results_with_versions_v3("serilog", ["2.3.0"]) end + its(:length) { is_expected.to eq(2) } describe "the first dependency" do @@ -88,7 +73,7 @@ def dependencies_from_info(deps_info) expect(dependency.requirements).to eq( [{ requirement: "1.0.1", - file: "proj.proj", + file: "my.not-csproj", groups: ["dependencies"], source: nil }] @@ -106,7 +91,7 @@ def dependencies_from_info(deps_info) expect(dependency.requirements).to eq( [{ requirement: "2.3.0", - file: "proj.proj", + file: "my.not-csproj", groups: ["dependencies"], source: nil }] @@ -116,24 +101,14 @@ def dependencies_from_info(deps_info) end context "with a single project file" do - let(:project_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.1.1", file: "my.csproj" }, - { name: "Microsoft.AspNetCore.App", version: nil, file: "my.csproj" }, - { name: "Microsoft.NET.Test.Sdk", version: nil, file: "my.csproj" }, - { name: "Microsoft.Extensions.PlatformAbstractions", version: "1.1.0", file: "my.csproj" }, - { name: "System.Collections.Specialized", version: "4.3.0", file: "my.csproj" } - ] - end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).and_return( - dependencies_from_info(project_dependencies) - ) + stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) + stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) + stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) + stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) + stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) end - its(:length) { is_expected.to eq(5) } + its(:length) { is_expected.to eq(3) } describe "the first dependency" do subject(:dependency) { top_level_dependencies.first } @@ -173,7 +148,7 @@ def dependencies_from_info(deps_info) end context "with a csproj and a vbproj" do - let(:files) { [csproj_file, vbproj_file] } + let(:additional_files) { [vbproj_file] } let(:vbproj_file) do Dependabot::DependencyFile.new( name: "my.vbproj", @@ -181,34 +156,15 @@ def dependencies_from_info(deps_info) ) end - let(:csproj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.1.1", file: "my.csproj" }, - { name: "Microsoft.AspNetCore.App", version: nil, file: "my.csproj" }, - { name: "Microsoft.NET.Test.Sdk", version: nil, file: "my.csproj" }, - { name: "Microsoft.Extensions.PlatformAbstractions", version: "1.1.0", file: "my.csproj" }, - { name: "System.Collections.Specialized", version: "4.3.0", file: "my.csproj" } - ] - end - - let(:vbproj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.0.1", file: "my.vbproj" }, - { name: "Serilog", version: "2.3.0", file: "my.vbproj" } - ] - end - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - allow(dummy_project_file_parser).to receive(:dependency_set).with(project_file: vbproj_file).and_return( - dependencies_from_info(vbproj_dependencies) - ) + stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) + stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) + stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) + stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) + stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) + stub_search_results_with_versions_v3("serilog", ["2.3.0"]) end - its(:length) { is_expected.to eq(6) } + its(:length) { is_expected.to eq(4) } describe "the first dependency" do subject(:dependency) { top_level_dependencies.first } @@ -253,7 +209,7 @@ def dependencies_from_info(deps_info) end context "with a packages.config" do - let(:files) { [packages_config] } + let(:additional_files) { [packages_config] } let(:packages_config) do Dependabot::DependencyFile.new( name: "packages.config", @@ -261,6 +217,14 @@ def dependencies_from_info(deps_info) ) end + let(:csproj_body) do + <<~XML + + + + XML + end + its(:length) { is_expected.to eq(9) } describe "the first dependency" do @@ -351,13 +315,7 @@ def dependencies_from_info(deps_info) end context "with a global.json" do - let(:files) { [packages_config, global_json] } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "packages.config", - content: fixture("packages_configs", "packages.config") - ) - end + let(:files) { [global_json] } let(:global_json) do Dependabot::DependencyFile.new( name: "global.json", @@ -365,7 +323,7 @@ def dependencies_from_info(deps_info) ) end - its(:length) { is_expected.to eq(10) } + its(:length) { is_expected.to eq(1) } describe "the last dependency" do subject(:dependency) { top_level_dependencies.last } @@ -387,13 +345,7 @@ def dependencies_from_info(deps_info) end context "with a dotnet-tools.json" do - let(:files) { [packages_config, dotnet_tools_json] } - let(:packages_config) do - Dependabot::DependencyFile.new( - name: "packages.config", - content: fixture("packages_configs", "packages.config") - ) - end + let(:files) { [dotnet_tools_json] } let(:dotnet_tools_json) do Dependabot::DependencyFile.new( name: ".config/dotnet-tools.json", @@ -401,7 +353,7 @@ def dependencies_from_info(deps_info) ) end - its(:length) { is_expected.to eq(11) } + its(:length) { is_expected.to eq(2) } describe "the last dependency" do subject(:dependency) { top_level_dependencies.last } @@ -423,7 +375,7 @@ def dependencies_from_info(deps_info) end context "with an imported properties file" do - let(:files) { [csproj_file, imported_file] } + let(:additional_files) { [imported_file] } let(:imported_file) do Dependabot::DependencyFile.new( name: "commonprops.props", @@ -431,35 +383,22 @@ def dependencies_from_info(deps_info) ) end - let(:csproj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.1.1", file: "my.csproj" }, - { name: "Microsoft.AspNetCore.App", version: nil, file: "my.csproj" }, - { name: "Microsoft.NET.Test.Sdk", version: nil, file: "my.csproj" }, - { name: "Microsoft.Extensions.PlatformAbstractions", version: "1.1.0", file: "my.csproj" }, - { name: "System.Collections.Specialized", version: "4.3.0", file: "my.csproj" } - ] - end - - let(:imported_file_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.0.1", file: "commonprops.props" }, - { name: "Serilog", version: "2.3.0", file: "commonprops.props" } - ] + let(:csproj_body) do + <<~XML + + + netstandard1.6 + + + + XML end before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: imported_file).and_return( - dependencies_from_info(imported_file_dependencies) - ) + stub_search_results_with_versions_v3("serilog", ["2.3.0"]) end - its(:length) { is_expected.to eq(6) } + its(:length) { is_expected.to eq(1) } describe "the last dependency" do subject(:dependency) { top_level_dependencies.last } @@ -481,7 +420,7 @@ def dependencies_from_info(deps_info) end context "with a packages.props file" do - let(:files) { [csproj_file, packages_file] } + let(:additional_files) { [packages_file] } let(:packages_file) do Dependabot::DependencyFile.new( name: "packages.props", @@ -489,38 +428,26 @@ def dependencies_from_info(deps_info) ) end - let(:csproj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.1.1", file: "my.csproj" }, - { name: "Microsoft.AspNetCore.App", version: nil, file: "my.csproj" }, - { name: "Microsoft.NET.Test.Sdk", version: nil, file: "my.csproj" }, - { name: "Microsoft.Extensions.PlatformAbstractions", version: "1.1.0", file: "my.csproj" }, - { name: "System.Collections.Specialized", version: "4.3.0", file: "my.csproj" } - ] - end - - let(:packages_file_dependencies) do - [ - { name: "Microsoft.SourceLink.GitHub", version: "1.0.0-beta2-19367-01", file: "packages.props" }, - { name: "System.Lycos", version: "3.23.3", file: "packages.props" }, - { name: "System.AskJeeves", version: "2.2.2", file: "packages.props" }, - { name: "System.Google", version: "0.1.0-beta.3", file: "packages.props" }, - { name: "System.WebCrawler", version: "1.1.1", file: "packages.props" } - ] + let(:csproj_body) do + <<~XML + + + netstandard1.6 + + + + XML end before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) + stub_search_results_with_versions_v3("microsoft.sourcelink.github", ["1.0.0-beta2-19367-01"]) + stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) + stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) + stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) + stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) end - its(:length) { is_expected.to eq(10) } + its(:length) { is_expected.to eq(5) } describe "the last dependency" do subject(:dependency) { top_level_dependencies.last } @@ -542,7 +469,7 @@ def dependencies_from_info(deps_info) end context "with a directory.packages.props file" do - let(:files) { [csproj_file, packages_file] } + let(:additional_files) { [packages_file] } let(:packages_file) do Dependabot::DependencyFile.new( name: "directory.packages.props", @@ -550,37 +477,30 @@ def dependencies_from_info(deps_info) ) end - let(:csproj_dependencies) do - [ - { name: "Microsoft.Extensions.DependencyModel", version: "1.1.1", file: "my.csproj" }, - { name: "Microsoft.AspNetCore.App", version: nil, file: "my.csproj" }, - { name: "Microsoft.NET.Test.Sdk", version: nil, file: "my.csproj" }, - { name: "Microsoft.Extensions.PlatformAbstractions", version: "1.1.0", file: "my.csproj" }, - { name: "System.Collections.Specialized", version: "4.3.0", file: "my.csproj" } - ] - end - - let(:packages_file_dependencies) do - [ - { name: "Microsoft.SourceLink.GitHub", version: "1.0.0-beta2-19367-01", file: "directory.packages.props" }, - { name: "System.Lycos", version: "3.23.3", file: "directory.packages.props" }, - { name: "System.AskJeeves", version: "2.2.2", file: "directory.packages.props" }, - { name: "System.WebCrawler", version: "1.1.1", file: "directory.packages.props" } - ] + let(:csproj_body) do + <<~XML + + + netstandard1.6 + + + + + + + + + XML end before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: csproj_file).and_return( - dependencies_from_info(csproj_dependencies) - ) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) + stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) + stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) + stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) + stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) end - its(:length) { is_expected.to eq(9) } + its(:length) { is_expected.to eq(4) } describe "the last dependency" do subject(:dependency) { top_level_dependencies.last } @@ -592,7 +512,7 @@ def dependencies_from_info(deps_info) expect(dependency.requirements).to eq( [{ requirement: "1.1.1", - file: "directory.packages.props", + file: "my.csproj", groups: ["dependencies"], source: nil }] @@ -610,41 +530,8 @@ def dependencies_from_info(deps_info) ) end - let(:packages_file_dependencies) do - [ - { name: "Microsoft.SourceLink.GitHub", version: "1.0.0-beta2-19367-01", file: "directory.packages.props" }, - { name: "System.Lycos", version: "3.23.3", file: "directory.packages.props" }, - { name: "System.AskJeeves", version: "2.2.2", file: "directory.packages.props" }, - { name: "System.WebCrawler", version: "1.1.1", file: "directory.packages.props" } - ] - end - - before do - dummy_project_file_parser = instance_double(described_class::ProjectFileParser) - allow(parser).to receive(:project_file_parser).and_return(dummy_project_file_parser) - expect(dummy_project_file_parser).to receive(:dependency_set).with(project_file: packages_file).and_return( - dependencies_from_info(packages_file_dependencies) - ) - end - - its(:length) { is_expected.to eq(4) } - - describe "the last dependency" do - subject(:dependency) { top_level_dependencies.last } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "directory.packages.props", - groups: ["dependencies"], - source: nil - }] - ) - end + it do + expect { dependencies }.to raise_error(Dependabot::DependencyFileNotFound) end end @@ -753,5 +640,122 @@ def dependencies_from_info(deps_info) ) end end + + context "with a property that can't be evaluated" do + let(:csproj_file) do + Dependabot::DependencyFile.new( + name: "my.csproj", + content: + <<~XML + + + $(SomeCommonTfmThatCannotBeResolved) + + + + + + XML + ) + end + + before do + allow(Dependabot.logger).to receive(:warn) + end + + it "does not return the `.csproj` with an unresolvable TFM" do + expect(dependencies.length).to eq(0) + expect(Dependabot.logger).to have_received(:warn).with( + "Excluding project file 'my.csproj' due to unresolvable target framework" + ) + end + end + + context "packages referenced in implicitly included `.targets` file are reported" do + let(:additional_files) { [directory_build_targets] } + let(:csproj_file) do + Dependabot::DependencyFile.new( + name: "my.csproj", + content: + <<~XML + + + net8.0 + + + + + + XML + ) + end + let(:directory_build_targets) do + Dependabot::DependencyFile.new( + name: "Directory.Build.targets", + content: + <<~XML + + + + + + XML + ) + end + + before do + stub_search_results_with_versions_v3("package.a", ["1.2.3"]) + stub_search_results_with_versions_v3("package.b", ["4.5.6"]) + end + + it "returns the correct dependency set" do + expect(dependencies.length).to eq(2) + expect(dependencies.map(&:name)).to match_array(%w(Package.A Package.B)) + expect(dependencies.map(&:version)).to match_array(%w(1.2.3 4.5.6)) + end + end + + context "project element can be resolved from implicitly imported file" do + let(:additional_files) { [directory_build_props] } + let(:csproj_file) do + Dependabot::DependencyFile.new( + name: "my.csproj", + content: + <<~XML + + + $(SomeTfm) + + + + + + XML + ) + end + let(:directory_build_props) do + Dependabot::DependencyFile.new( + name: "Directory.Build.props", + content: + <<~XML + + + net8.0 + + + XML + ) + end + + before do + stub_search_results_with_versions_v3("package.a", ["1.2.3"]) + end + + it "returns the correct dependency set" do + expect(dependencies.length).to eq(1) + expect(dependencies[0].name).to eq("Package.A") + expect(dependencies[0].version).to eq("1.2.3") + end + end end end diff --git a/nuget/spec/fixtures/csproj/basic2.csproj b/nuget/spec/fixtures/csproj/basic2.csproj index b36663f7db7..fa40df37240 100644 --- a/nuget/spec/fixtures/csproj/basic2.csproj +++ b/nuget/spec/fixtures/csproj/basic2.csproj @@ -1,4 +1,7 @@ + + netstandard1.6 +