From 0585f2d29d6bb084ace546799469874966b4780f Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Mon, 8 Mar 2021 13:36:42 +0100 Subject: [PATCH] fix(nuget): parse nested conditional deps (#9035) --- lib/manager/nuget/__fixtures__/sample.csproj | 12 ++ .../nuget/__snapshots__/extract.spec.ts.snap | 158 +++++++++--------- lib/manager/nuget/extract.spec.ts | 6 +- lib/manager/nuget/extract.ts | 33 ++-- 4 files changed, 120 insertions(+), 89 deletions(-) diff --git a/lib/manager/nuget/__fixtures__/sample.csproj b/lib/manager/nuget/__fixtures__/sample.csproj index 1989dcb75d03fb..8bf93b13f50425 100644 --- a/lib/manager/nuget/__fixtures__/sample.csproj +++ b/lib/manager/nuget/__fixtures__/sample.csproj @@ -46,4 +46,16 @@ PreserveNewest + + + + + + + + + + + + diff --git a/lib/manager/nuget/__snapshots__/extract.spec.ts.snap b/lib/manager/nuget/__snapshots__/extract.spec.ts.snap index c03d7371467f45..f37a61bb873712 100644 --- a/lib/manager/nuget/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/nuget/__snapshots__/extract.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`lib/manager/nuget/extract extractPackageFile() .config/dotnet-tools.json with-config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() .config/dotnet-tools.json with-config 1`] = ` Object { "deps": Array [ Object { @@ -17,7 +17,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() .config/dotnet-tools.json works 1`] = ` +exports[`manager/nuget/extract extractPackageFile() .config/dotnet-tools.json works 1`] = ` Object { "deps": Array [ Object { @@ -30,7 +30,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() considers NuGet.config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() considers NuGet.config 1`] = ` Object { "deps": Array [ Object { @@ -47,7 +47,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() considers lower-case nuget.config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() considers lower-case nuget.config 1`] = ` Object { "deps": Array [ Object { @@ -64,7 +64,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() considers pascal-case NuGet.Config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() considers pascal-case NuGet.Config 1`] = ` Object { "deps": Array [ Object { @@ -81,54 +81,54 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() extracts all dependencies 1`] = ` +exports[`manager/nuget/extract extractPackageFile() extracts all dependencies 1`] = ` Array [ Object { - "currentValue": "4.5.0", + "currentValue": "1.0.0", "datasource": "nuget", - "depName": "Autofac", + "depName": "My.Package", "depType": "nuget", }, Object { - "currentValue": "4.1.0", + "currentValue": "1.0.0", "datasource": "nuget", - "depName": "Autofac.Extensions.DependencyInjection", + "depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Hosting", + "depName": "Range3", "depType": "nuget", }, Object { - "currentValue": "1.1.3", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Mvc.Core", + "depName": "Range2", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Server.Kestrel", + "depName": "Range1", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "3.1.0.5", "datasource": "nuget", - "depName": "Microsoft.Extensions.Configuration.Json", + "depName": "Stateless", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "2.1.0", "datasource": "nuget", - "depName": "Microsoft.Extensions.Logging.Debug", + "depName": "Serilog.Sinks.Literate", "depType": "nuget", }, Object { - "currentValue": "10.0.2", + "currentValue": "1.4.0", "datasource": "nuget", - "depName": "Newtonsoft.Json", + "depName": "Serilog.Extensions.Logging", "depType": "nuget", }, Object { @@ -138,98 +138,104 @@ Array [ "depType": "nuget", }, Object { - "currentValue": "1.4.0", + "currentValue": "10.0.2", "datasource": "nuget", - "depName": "Serilog.Extensions.Logging", + "depName": "Newtonsoft.Json", "depType": "nuget", }, Object { - "currentValue": "2.1.0", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Serilog.Sinks.Literate", + "depName": "Microsoft.Extensions.Logging.Debug", "depType": "nuget", }, Object { - "currentValue": "3.1.0.5", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Stateless", + "depName": "Microsoft.Extensions.Configuration.Json", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Range1", + "depName": "Microsoft.AspNetCore.Server.Kestrel", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "1.1.3", "datasource": "nuget", - "depName": "Range2", + "depName": "Microsoft.AspNetCore.Mvc.Core", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Range3", + "depName": "Microsoft.AspNetCore.Hosting", "depType": "nuget", }, Object { - "currentValue": "1.0.0", + "currentValue": "4.1.0", "datasource": "nuget", - "depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools", + "depName": "Autofac.Extensions.DependencyInjection", + "depType": "nuget", + }, + Object { + "currentValue": "4.5.0", + "datasource": "nuget", + "depName": "Autofac", "depType": "nuget", }, ] `; -exports[`lib/manager/nuget/extract extractPackageFile() extracts all dependencies from global packages file 1`] = ` +exports[`manager/nuget/extract extractPackageFile() extracts all dependencies from global packages file 1`] = ` Array [ Object { - "currentValue": "2.0.0", + "currentValue": "1.0.0", "datasource": "nuget", - "depName": "Roslynator.Analyzers", + "depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools", "depType": "nuget", }, Object { - "currentValue": "4.5.0", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Autofac", + "depName": "Range3", "depType": "nuget", }, Object { - "currentValue": "4.1.0", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Autofac.Extensions.DependencyInjection", + "depName": "Range2", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "1.2.3", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Hosting", + "depName": "Range1", "depType": "nuget", }, Object { - "currentValue": "1.1.3", + "currentValue": "3.1.0.5", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Mvc.Core", + "depName": "Stateless", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "2.1.0", "datasource": "nuget", - "depName": "Microsoft.AspNetCore.Server.Kestrel", + "depName": "Serilog.Sinks.Literate", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "1.4.0", "datasource": "nuget", - "depName": "Microsoft.Extensions.Configuration.Json", + "depName": "Serilog.Extensions.Logging", "depType": "nuget", }, Object { - "currentValue": "1.1.2", + "currentValue": "2.4.0", "datasource": "nuget", - "depName": "Microsoft.Extensions.Logging.Debug", + "depName": "Serilog", "depType": "nuget", }, Object { @@ -239,57 +245,57 @@ Array [ "depType": "nuget", }, Object { - "currentValue": "2.4.0", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Serilog", + "depName": "Microsoft.Extensions.Logging.Debug", "depType": "nuget", }, Object { - "currentValue": "1.4.0", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Serilog.Extensions.Logging", + "depName": "Microsoft.Extensions.Configuration.Json", "depType": "nuget", }, Object { - "currentValue": "2.1.0", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Serilog.Sinks.Literate", + "depName": "Microsoft.AspNetCore.Server.Kestrel", "depType": "nuget", }, Object { - "currentValue": "3.1.0.5", + "currentValue": "1.1.3", "datasource": "nuget", - "depName": "Stateless", + "depName": "Microsoft.AspNetCore.Mvc.Core", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "1.1.2", "datasource": "nuget", - "depName": "Range1", + "depName": "Microsoft.AspNetCore.Hosting", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "4.1.0", "datasource": "nuget", - "depName": "Range2", + "depName": "Autofac.Extensions.DependencyInjection", "depType": "nuget", }, Object { - "currentValue": "1.2.3", + "currentValue": "4.5.0", "datasource": "nuget", - "depName": "Range3", + "depName": "Autofac", "depType": "nuget", }, Object { - "currentValue": "1.0.0", + "currentValue": "2.0.0", "datasource": "nuget", - "depName": "Microsoft.VisualStudio.Web.CodeGeneration.Tools", + "depName": "Roslynator.Analyzers", "depType": "nuget", }, ] `; -exports[`lib/manager/nuget/extract extractPackageFile() extracts package version dependency 1`] = ` +exports[`manager/nuget/extract extractPackageFile() extracts package version dependency 1`] = ` Array [ Object { "currentValue": "4.5.0", @@ -300,7 +306,7 @@ Array [ ] `; -exports[`lib/manager/nuget/extract extractPackageFile() extracts registry URLs independently 1`] = ` +exports[`manager/nuget/extract extractPackageFile() extracts registry URLs independently 1`] = ` Object { "deps": Array [ Object { @@ -317,7 +323,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() extracts registry URLs independently 2`] = ` +exports[`manager/nuget/extract extractPackageFile() extracts registry URLs independently 2`] = ` Object { "deps": Array [ Object { @@ -334,7 +340,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() handles NuGet.config without package sources 1`] = ` +exports[`manager/nuget/extract extractPackageFile() handles NuGet.config without package sources 1`] = ` Object { "deps": Array [ Object { @@ -347,7 +353,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() handles malformed NuGet.config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() handles malformed NuGet.config 1`] = ` Object { "deps": Array [ Object { @@ -360,7 +366,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() ignores local feed in NuGet.config 1`] = ` +exports[`manager/nuget/extract extractPackageFile() ignores local feed in NuGet.config 1`] = ` Object { "deps": Array [ Object { @@ -376,7 +382,7 @@ Object { } `; -exports[`lib/manager/nuget/extract extractPackageFile() returns empty for invalid csproj 1`] = ` +exports[`manager/nuget/extract extractPackageFile() returns empty for invalid csproj 1`] = ` Object { "deps": Array [], } diff --git a/lib/manager/nuget/extract.spec.ts b/lib/manager/nuget/extract.spec.ts index d1fa474f304b64..74494f376480ca 100644 --- a/lib/manager/nuget/extract.spec.ts +++ b/lib/manager/nuget/extract.spec.ts @@ -1,9 +1,10 @@ import { readFileSync } from 'fs'; import * as upath from 'upath'; +import { getName } from '../../../test/util'; import type { ExtractConfig } from '../types'; import { extractPackageFile } from './extract'; -describe('lib/manager/nuget/extract', () => { +describe(getName(__filename), () => { describe('extractPackageFile()', () => { let config: ExtractConfig; beforeEach(() => { @@ -25,6 +26,7 @@ describe('lib/manager/nuget/extract', () => { ); const res = await extractPackageFile(sample, packageFile, config); expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(1); }); it('extracts all dependencies', async () => { const packageFile = 'sample.csproj'; @@ -34,6 +36,7 @@ describe('lib/manager/nuget/extract', () => { ); const res = await extractPackageFile(sample, packageFile, config); expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(17); }); it('extracts all dependencies from global packages file', async () => { const packageFile = 'packages.props'; @@ -43,6 +46,7 @@ describe('lib/manager/nuget/extract', () => { ); const res = await extractPackageFile(sample, packageFile, config); expect(res.deps).toMatchSnapshot(); + expect(res.deps).toHaveLength(17); }); it('considers NuGet.config', async () => { const packageFile = 'with-config-file/with-config-file.csproj'; diff --git a/lib/manager/nuget/extract.ts b/lib/manager/nuget/extract.ts index 74fd8c1493767e..3ed7fde2ca5b78 100644 --- a/lib/manager/nuget/extract.ts +++ b/lib/manager/nuget/extract.ts @@ -1,7 +1,8 @@ -import { XmlDocument } from 'xmldoc'; +import { XmlDocument, XmlElement, XmlNode } from 'xmldoc'; import * as datasourceNuget from '../../datasource/nuget'; import { logger } from '../../logger'; import { getSiblingFileName, localPathExists } from '../../util/fs'; +import { hasKey } from '../../util/object'; import type { ExtractConfig, PackageDependency, PackageFile } from '../types'; import type { DotnetToolsManifest } from './types'; import { getConfiguredRegistries } from './util'; @@ -18,19 +19,25 @@ import { getConfiguredRegistries } from './util'; * so we don't include it in the extracting regexp */ const checkVersion = /^\s*(?:[[])?(?:(?[^"(,[\]]+)\s*(?:,\s*[)\]]|])?)\s*$/; +const elemNames = new Set([ + 'PackageReference', + 'PackageVersion', + 'DotNetCliToolReference', + 'GlobalPackageReference', +]); + +function isXmlElem(node: XmlNode): boolean { + return hasKey('name', node); +} function extractDepsFromXml(xmlNode: XmlDocument): PackageDependency[] { - const results = []; - const itemGroups = xmlNode.childrenNamed('ItemGroup'); - for (const itemGroup of itemGroups) { - const relevantChildren = [ - ...itemGroup.childrenNamed('PackageReference'), - ...itemGroup.childrenNamed('PackageVersion'), - ...itemGroup.childrenNamed('DotNetCliToolReference'), - ...itemGroup.childrenNamed('GlobalPackageReference'), - ]; - for (const child of relevantChildren) { - const { attr } = child; + const results: PackageDependency[] = []; + const todo: XmlElement[] = [xmlNode]; + while (todo.length) { + const child = todo.pop(); + const { name, attr } = child; + + if (elemNames.has(name)) { const depName = attr?.Include || attr?.Update; const version = attr?.Version || @@ -48,6 +55,8 @@ function extractDepsFromXml(xmlNode: XmlDocument): PackageDependency[] { currentValue, }); } + } else { + todo.push(...(child.children.filter(isXmlElem) as XmlElement[])); } } return results;