From 12ac34bfa6352158014cb11bbc7cc07bc5e2ed20 Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Thu, 28 May 2020 06:01:05 +0200
Subject: [PATCH] feat(dotnet): support dotnet-tools (#6335)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 .../with-config-file/NuGet.config             |  1 +
 .../nuget/__snapshots__/extract.spec.ts.snap  | 30 ++++++++++++
 lib/manager/nuget/extract.spec.ts             | 46 ++++++++++++++++++-
 lib/manager/nuget/extract.ts                  | 38 ++++++++++++++-
 lib/manager/nuget/index.ts                    |  6 ++-
 lib/manager/nuget/types.ts                    | 11 +++++
 6 files changed, 129 insertions(+), 3 deletions(-)
 create mode 100644 lib/manager/nuget/types.ts

diff --git a/lib/manager/nuget/__fixtures__/with-config-file/NuGet.config b/lib/manager/nuget/__fixtures__/with-config-file/NuGet.config
index 6cb726de41..77da4bd6be 100644
--- a/lib/manager/nuget/__fixtures__/with-config-file/NuGet.config
+++ b/lib/manager/nuget/__fixtures__/with-config-file/NuGet.config
@@ -4,5 +4,6 @@
     <clear />
     <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
     <add key="Contoso" value="https://contoso.com/packages/" />
+    <remove key="test" />
   </packageSources>
 </configuration>
diff --git a/lib/manager/nuget/__snapshots__/extract.spec.ts.snap b/lib/manager/nuget/__snapshots__/extract.spec.ts.snap
index e2e788f8a2..e90466b838 100644
--- a/lib/manager/nuget/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/nuget/__snapshots__/extract.spec.ts.snap
@@ -1,5 +1,35 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`lib/manager/nuget/extract extractPackageFile() .config/dotnet-tools.json with-config 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentValue": "2.0.0",
+      "datasource": "nuget",
+      "depName": "minver-cli",
+      "depType": "nuget",
+      "registryUrls": Array [
+        "https://api.nuget.org/v3/index.json#protocolVersion=3",
+        "https://contoso.com/packages/",
+      ],
+    },
+  ],
+}
+`;
+
+exports[`lib/manager/nuget/extract extractPackageFile() .config/dotnet-tools.json works 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentValue": "2.0.0",
+      "datasource": "nuget",
+      "depName": "minver-cli",
+      "depType": "nuget",
+    },
+  ],
+}
+`;
+
 exports[`lib/manager/nuget/extract extractPackageFile() considers NuGet.config 1`] = `
 Object {
   "deps": Array [
diff --git a/lib/manager/nuget/extract.spec.ts b/lib/manager/nuget/extract.spec.ts
index e7733fb420..926cac2b60 100644
--- a/lib/manager/nuget/extract.spec.ts
+++ b/lib/manager/nuget/extract.spec.ts
@@ -1,10 +1,11 @@
 import { readFileSync } from 'fs';
 import * as path from 'path';
+import { ExtractConfig } from '../common';
 import { extractPackageFile } from './extract';
 
 describe('lib/manager/nuget/extract', () => {
   describe('extractPackageFile()', () => {
-    let config;
+    let config: ExtractConfig;
     beforeEach(() => {
       config = {
         localDir: path.resolve('lib/manager/nuget/__fixtures__'),
@@ -84,5 +85,48 @@ describe('lib/manager/nuget/extract', () => {
         await extractPackageFile(contents, packageFile, config)
       ).toMatchSnapshot();
     });
+
+    describe('.config/dotnet-tools.json', () => {
+      const packageFile = '.config/dotnet-tools.json';
+      const contents = `{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "minver-cli": {
+      "version": "2.0.0",
+      "commands": ["minver"]
+    }
+  }
+}`;
+      it('works', async () => {
+        expect(
+          await extractPackageFile(contents, packageFile, config)
+        ).toMatchSnapshot();
+      });
+
+      it('with-config', async () => {
+        expect(
+          await extractPackageFile(
+            contents,
+            `with-config-file/${packageFile}`,
+            config
+          )
+        ).toMatchSnapshot();
+      });
+
+      it('wrong version', async () => {
+        expect(
+          await extractPackageFile(
+            contents.replace('"version": 1,', '"version": 2,'),
+            packageFile,
+            config
+          )
+        ).toBeNull();
+      });
+
+      it('does not throw', async () => {
+        expect(await extractPackageFile('{{', packageFile, config)).toBeNull();
+      });
+    });
   });
 });
diff --git a/lib/manager/nuget/extract.ts b/lib/manager/nuget/extract.ts
index 116a2c023a..c6403ed4bd 100644
--- a/lib/manager/nuget/extract.ts
+++ b/lib/manager/nuget/extract.ts
@@ -8,6 +8,7 @@ import { SkipReason } from '../../types';
 import { get } from '../../versioning';
 import * as semverVersioning from '../../versioning/semver';
 import { ExtractConfig, PackageDependency, PackageFile } from '../common';
+import { DotnetToolsManifest } from './types';
 
 async function readFileAsXmlDocument(file: string): Promise<XmlDocument> {
   try {
@@ -58,6 +59,7 @@ async function determineRegistryUrls(
         logger.debug({ registryUrl }, 'adding registry URL');
         registryUrls.push(registryUrl);
       }
+      // child.name === 'remove' not supported
     }
   }
   return registryUrls;
@@ -68,7 +70,7 @@ export async function extractPackageFile(
   content: string,
   packageFile: string,
   config: ExtractConfig
-): Promise<PackageFile> {
+): Promise<PackageFile | null> {
   logger.trace({ packageFile }, 'nuget.extractPackageFile()');
   const { isVersion } = get(config.versioning || semverVersioning.id);
   const deps: PackageDependency[] = [];
@@ -78,6 +80,40 @@ export async function extractPackageFile(
     config.localDir
   );
 
+  if (packageFile.endsWith('.config/dotnet-tools.json')) {
+    let manifest: DotnetToolsManifest;
+
+    try {
+      manifest = JSON.parse(content);
+    } catch (err) {
+      logger.debug({ fileName: packageFile }, 'Invalid JSON');
+      return null;
+    }
+
+    if (manifest.version !== 1) {
+      logger.debug({ contents: manifest }, 'Unsupported dotnet tools version');
+      return null;
+    }
+
+    for (const depName of Object.keys(manifest.tools)) {
+      const tool = manifest.tools[depName];
+      const currentValue = tool.version;
+      const dep: PackageDependency = {
+        depType: 'nuget',
+        depName,
+        currentValue,
+        datasource: datasourceNuget.id,
+      };
+      if (registryUrls) {
+        dep.registryUrls = registryUrls;
+      }
+
+      deps.push(dep);
+    }
+
+    return { deps };
+  }
+
   for (const line of content.split('\n')) {
     /**
      * https://docs.microsoft.com/en-us/nuget/concepts/package-versioning
diff --git a/lib/manager/nuget/index.ts b/lib/manager/nuget/index.ts
index f42cb60352..32d3de6916 100644
--- a/lib/manager/nuget/index.ts
+++ b/lib/manager/nuget/index.ts
@@ -5,5 +5,9 @@ export { extractPackageFile } from './extract';
 export const language = LANGUAGE_DOT_NET;
 
 export const defaultConfig = {
-  fileMatch: ['\\.(?:cs|fs|vb)proj$'],
+  fileMatch: [
+    '\\.(?:cs|fs|vb)proj$',
+    '\\.(?:props|targets)$',
+    '\\.config\\/dotnet-tools\\.json$',
+  ],
 };
diff --git a/lib/manager/nuget/types.ts b/lib/manager/nuget/types.ts
new file mode 100644
index 0000000000..e2cbb13bf9
--- /dev/null
+++ b/lib/manager/nuget/types.ts
@@ -0,0 +1,11 @@
+export interface DotnetToolsManifest {
+  readonly version: number;
+  readonly isRoot: boolean;
+
+  readonly tools: Record<string, DotnetTool>;
+}
+
+export interface DotnetTool {
+  readonly version: string;
+  readonly commands: string[];
+}
-- 
GitLab