Require Visual Studio 2019 for Windows (#48844)

diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 178366d..3e10806 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -196,7 +196,11 @@
   String visualStudioMissing(String workload, List<String> components) =>
       'Visual Studio not installed; this is necessary for Windows development.\n'
       'Download at https://visualstudio.microsoft.com/downloads/.\n'
-      'Please install the "$workload" workload, including following components:\n  ${components.join('\n  ')}';
+      'Please install the "$workload" workload, including the following components:\n  ${components.join('\n  ')}';
+  String visualStudioTooOld(String minimumVersion, String workload, List<String> components) =>
+      'Visual Studio $minimumVersion or later is required.\n'
+      'Download at https://visualstudio.microsoft.com/downloads/.\n'
+      'Please install the "$workload" workload, including the following components:\n  ${components.join('\n  ')}';
   String get visualStudioIsPrerelease => 'The current Visual Studio installation is a pre-release version. It may not be '
       'supported by Flutter yet.';
   String get visualStudioNotLaunchable =>
diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart
index 1973d4e..93e79c0 100644
--- a/packages/flutter_tools/lib/src/windows/visual_studio.dart
+++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart
@@ -12,20 +12,25 @@
 
 /// Encapsulates information about the installed copy of Visual Studio, if any.
 class VisualStudio {
-  /// True if a sufficiently recent version of Visual Studio is installed.
+  /// True if Visual Studio installation was found.
   ///
   /// Versions older than 2017 Update 2 won't be detected, so error messages to
   /// users should take into account that [false] may mean that the user may
   /// have an old version rather than no installation at all.
   bool get isInstalled => _bestVisualStudioDetails.isNotEmpty;
 
+  bool get isAtLeastMinimumVersion {
+    final int installedMajorVersion = _majorVersion;
+    return installedMajorVersion != null && installedMajorVersion >= _minimumSupportedVersion;
+  }
+
   /// True if there is a version of Visual Studio with all the components
   /// necessary to build the project.
   bool get hasNecessaryComponents => _usableVisualStudioDetails.isNotEmpty;
 
   /// The name of the Visual Studio install.
   ///
-  /// For instance: "Visual Studio Community 2017".
+  /// For instance: "Visual Studio Community 2019".
   String get displayName => _bestVisualStudioDetails[_displayNameKey] as String;
 
   /// The user-friendly version number of the Visual Studio install.
@@ -81,11 +86,18 @@
 
   /// The names of the components within the workload that must be installed.
   ///
-  /// If there is an existing Visual Studio installation, the major version
-  /// should be provided here, as the descriptions of some components differ
-  /// from version to version.
-  List<String> necessaryComponentDescriptions([int visualStudioMajorVersion]) {
-    return _requiredComponents(visualStudioMajorVersion).values.toList();
+  /// The descriptions of some components differ from version to version. When
+  /// a supported version is present, the descriptions used will be for that
+  /// version.
+  List<String> necessaryComponentDescriptions() {
+    return _requiredComponents().values.toList();
+  }
+
+  /// The consumer-facing version name of the minumum supported version.
+  ///
+  /// E.g., for Visual Studio 2019 this returns "2019" rather than "16".
+  String get minimumVersionDescription {
+    return '2019';
   }
 
   /// The path to vcvars64.bat, or null if no Visual Studio installation has
@@ -104,6 +116,9 @@
     );
   }
 
+  /// The major version of the Visual Studio install, as an integer.
+  int get _majorVersion => fullVersion != null ? int.tryParse(fullVersion.split('.')[0]) : null;
+
   /// The path to vswhere.exe.
   ///
   /// vswhere should be installed for VS 2017 Update 2 and later; if it's not
@@ -121,14 +136,18 @@
   ///
   /// Maps from component IDs to description in the installer UI.
   /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
-  Map<String, String> _requiredComponents([int visualStudioMajorVersion]) {
+  Map<String, String> _requiredComponents([int majorVersion]) {
     // The description of the C++ toolchain required by the template. The
     // component name is significantly different in different versions.
-    // Default to the latest known description, but use a specific string
-    // if a known older version is requested.
-    String cppToolchainDescription = 'MSVC v142 - VS 2019 C++ x64/x86 build tools';
-    if (visualStudioMajorVersion == 15) {
-      cppToolchainDescription = 'VC++ 2017 version 15.9 v14.## latest v141 tools';
+    // When a new major version of VS is supported, its toochain description
+    // should be added below. It should also be made the default, so that when
+    // there is no installation, the message shows the string that will be
+    // relevant for the most likely fresh install case).
+    String cppToolchainDescription;
+    switch (majorVersion ?? _majorVersion) {
+      case 16:
+      default:
+        cppToolchainDescription = 'MSVC v142 - VS 2019 C++ x64/x86 build tools';
     }
     // The 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' ID is assigned to the latest
     // release of the toolchain, and there can be minor updates within a given version of
@@ -147,6 +166,15 @@
     };
   }
 
+  /// The minimum supported major version.
+  static const int _minimumSupportedVersion = 16;  // '16' is VS 2019.
+
+  /// vswhere argument to specificy the minimum version.
+  static const String _vswhereMinVersionArgument = '-version';
+
+  /// vswhere argument to allow prerelease versions.
+  static const String _vswherePrereleaseArgument = '-prerelease';
+
   // Keys in a VS details dictionary returned from vswhere.
 
   /// The root directory of the Visual Studio installation.
@@ -174,9 +202,6 @@
   /// This key is under the 'catalog' entry.
   static const String _catalogDisplayVersionKey = 'productDisplayVersion';
 
-  /// vswhere argument keys
-  static const String _prereleaseKey = '-prerelease';
-
   /// Returns the details dictionary for the newest version of Visual Studio
   /// that includes all of [requiredComponents], if there is one.
   Map<String, dynamic> _visualStudioDetails(
@@ -235,7 +260,8 @@
   }
 
   /// Returns the details dictionary for the latest version of Visual Studio
-  /// that has all required components, or {} if there is no such installation.
+  /// that has all required components and is a supported version, or {} if
+  /// there is no such installation.
   ///
   /// If no installation is found, the cached VS details are set to an empty map
   /// to avoid repeating vswhere queries that have already not found an installation.
@@ -244,12 +270,17 @@
     if (_cachedUsableVisualStudioDetails != null) {
       return _cachedUsableVisualStudioDetails;
     }
-    Map<String, dynamic> visualStudioDetails =
-        _visualStudioDetails(requiredComponents: _requiredComponents().keys);
+    final List<String> minimumVersionArguments = <String>[
+      _vswhereMinVersionArgument,
+      _minimumSupportedVersion.toString(),
+    ];
+    Map<String, dynamic> visualStudioDetails = _visualStudioDetails(
+        requiredComponents: _requiredComponents(_minimumSupportedVersion).keys,
+        additionalArguments: minimumVersionArguments);
     // If a stable version is not found, try searching for a pre-release version.
     visualStudioDetails ??= _visualStudioDetails(
-        requiredComponents: _requiredComponents().keys,
-        additionalArguments: <String>[_prereleaseKey]);
+        requiredComponents: _requiredComponents(_minimumSupportedVersion).keys,
+        additionalArguments: <String>[...minimumVersionArguments, _vswherePrereleaseArgument]);
 
     if (visualStudioDetails != null) {
       if (installationHasIssues(visualStudioDetails)) {
@@ -263,16 +294,17 @@
   }
 
   /// Returns the details dictionary of the latest version of Visual Studio,
-  /// regardless of components, or {} if no such installation is found.
+  /// regardless of components and version, or {} if no such installation is
+  /// found.
   ///
-  /// If no installation is found, the cached
-  /// VS details are set to an empty map to avoid repeating vswhere queries that
-  /// have already not found an installation.
+  /// If no installation is found, the cached VS details are set to an empty map
+  /// to avoid repeating vswhere queries that have already not found an
+  /// installation.
   Map<String, dynamic> _cachedAnyVisualStudioDetails;
   Map<String, dynamic> get _anyVisualStudioDetails {
     // Search for all types of installations.
     _cachedAnyVisualStudioDetails ??= _visualStudioDetails(
-        additionalArguments: <String>[_prereleaseKey, '-all']);
+        additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
     // Add a sentinel empty value to avoid querying vswhere again.
     _cachedAnyVisualStudioDetails ??= <String, dynamic>{};
     return _cachedAnyVisualStudioDetails;
diff --git a/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart
index 6319bd3..81ccdaa 100644
--- a/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart
+++ b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart
@@ -12,10 +12,6 @@
 class VisualStudioValidator extends DoctorValidator {
   const VisualStudioValidator() : super('Visual Studio - develop for Windows');
 
-  int get majorVersion => visualStudio.fullVersion != null
-      ? int.tryParse(visualStudio.fullVersion.split('.')[0])
-      : null;
-
   @override
   Future<ValidationResult> validate() async {
     final List<ValidationMessage> messages = <ValidationMessage>[];
@@ -39,7 +35,16 @@
       }
 
       // Messages for faulty installations.
-      if (visualStudio.isRebootRequired) {
+      if (!visualStudio.isAtLeastMinimumVersion) {
+        status = ValidationType.partial;
+        messages.add(ValidationMessage.error(
+            userMessages.visualStudioTooOld(
+                visualStudio.minimumVersionDescription,
+                visualStudio.workloadDescription,
+                visualStudio.necessaryComponentDescriptions(),
+            ),
+        ));
+      } else if (visualStudio.isRebootRequired) {
         status = ValidationType.partial;
         messages.add(ValidationMessage.error(userMessages.visualStudioRebootRequired));
       } else if (!visualStudio.isComplete) {
@@ -48,12 +53,12 @@
       } else if (!visualStudio.isLaunchable) {
         status = ValidationType.partial;
         messages.add(ValidationMessage.error(userMessages.visualStudioNotLaunchable));
-      } else  if (!visualStudio.hasNecessaryComponents) {
+      } else if (!visualStudio.hasNecessaryComponents) {
         status = ValidationType.partial;
         messages.add(ValidationMessage.error(
             userMessages.visualStudioMissingComponents(
                 visualStudio.workloadDescription,
-                visualStudio.necessaryComponentDescriptions(majorVersion),
+                visualStudio.necessaryComponentDescriptions(),
             ),
         ));
       }
@@ -63,7 +68,7 @@
       messages.add(ValidationMessage.error(
         userMessages.visualStudioMissing(
           visualStudio.workloadDescription,
-          visualStudio.necessaryComponentDescriptions(majorVersion),
+          visualStudio.necessaryComponentDescriptions(),
         ),
       ));
     }
diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart
index 008d1b0..3534c86 100644
--- a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart
+++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart
@@ -48,17 +48,31 @@
     },
   };
 
-  // A version of a response that doesn't include certain installation status
-  // information that might be missing in older Visual Studio versions.
-  const Map<String, dynamic> _missingStatusResponse = <String, dynamic>{
+  // A response for a VS installation that's too old.
+  const Map<String, dynamic> _tooOldResponse = <String, dynamic>{
     'installationPath': visualStudioPath,
     'displayName': 'Visual Studio Community 2017',
     'installationVersion': '15.9.28307.665',
+    'isRebootRequired': false,
+    'isComplete': true,
+    'isLaunchable': true,
+    'isPrerelease': false,
     'catalog': <String, dynamic>{
       'productDisplayVersion': '15.9.12',
     },
   };
 
+  // A version of a response that doesn't include certain installation status
+  // information that might be missing in older vswhere.
+  const Map<String, dynamic> _missingStatusResponse = <String, dynamic>{
+    'installationPath': visualStudioPath,
+    'displayName': 'Visual Studio Community 2017',
+    'installationVersion': '16.4.29609.76',
+    'catalog': <String, dynamic>{
+      'productDisplayVersion': '16.4.1',
+    },
+  };
+
   // Arguments for a vswhere query to search for an installation with the required components.
   const List<String> _requiredComponents = <String>[
     'Microsoft.Component.MSBuild',
@@ -108,13 +122,13 @@
   // Sets whether or not a vswhere query with the required components will
   // return an installation.
   void setMockCompatibleVisualStudioInstallation(Map<String, dynamic>response) {
-    setMockVswhereResponse(_requiredComponents, null, response);
+    setMockVswhereResponse(_requiredComponents, <String>['-version', '16'], response);
   }
 
   // Sets whether or not a vswhere query with the required components will
   // return a pre-release installation.
   void setMockPrereleaseVisualStudioInstallation(Map<String, dynamic>response) {
-    setMockVswhereResponse(_requiredComponents, <String>['-prerelease'], response);
+    setMockVswhereResponse(_requiredComponents, <String>['-version', '16', '-prerelease'], response);
   }
 
   // Sets whether or not a vswhere query searching for 'all' and 'prerelease'
@@ -200,6 +214,7 @@
 
       visualStudio = VisualStudio();
       expect(visualStudio.isInstalled, false);
+      expect(visualStudio.isAtLeastMinimumVersion, false);
       expect(visualStudio.hasNecessaryComponents, false);
       expect(visualStudio.isComplete, false);
       expect(visualStudio.isRebootRequired, false);
@@ -214,21 +229,25 @@
       Platform: () => windowsPlatform,
     });
 
-    testUsingContext('necessaryComponentDescriptions suggest the right VS tools on major version 15', () {
+    testUsingContext('necessaryComponentDescriptions suggest the right VS tools on major version 16', () {
+      setMockCompatibleVisualStudioInstallation(_defaultResponse);
 
       visualStudio = VisualStudio();
-      final String toolsString = visualStudio.necessaryComponentDescriptions(15)[1];
-      expect(toolsString.contains('v141'), true);
+      final String toolsString = visualStudio.necessaryComponentDescriptions()[1];
+      expect(toolsString.contains('v142'), true);
     }, overrides: <Type, Generator>{
       FileSystem: () => memoryFilesystem,
       ProcessManager: () => mockProcessManager,
       Platform: () => windowsPlatform,
     });
 
-    testUsingContext('necessaryComponentDescriptions suggest the right VS tools on major version != 15', () {
+    testUsingContext('necessaryComponentDescriptions suggest the right VS tools on an old version', () {
+      setMockCompatibleVisualStudioInstallation(null);
+      setMockPrereleaseVisualStudioInstallation(null);
+      setMockAnyVisualStudioInstallation(_tooOldResponse);
 
       visualStudio = VisualStudio();
-      final String toolsString = visualStudio.necessaryComponentDescriptions(16)[1];
+      final String toolsString = visualStudio.necessaryComponentDescriptions()[1];
       expect(toolsString.contains('v142'), true);
     }, overrides: <Type, Generator>{
       FileSystem: () => memoryFilesystem,
@@ -262,6 +281,19 @@
       Platform: () => windowsPlatform,
     });
 
+    testUsingContext('isInstalled returns true when VS is present but too old', () {
+      setMockCompatibleVisualStudioInstallation(null);
+      setMockPrereleaseVisualStudioInstallation(null);
+      setMockAnyVisualStudioInstallation(_tooOldResponse);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, true);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      ProcessManager: () => mockProcessManager,
+      Platform: () => windowsPlatform,
+    });
+
     testUsingContext('isInstalled returns true when a prerelease version of VS is present', () {
       setMockCompatibleVisualStudioInstallation(null);
       setMockAnyVisualStudioInstallation(null);
@@ -279,6 +311,20 @@
       Platform: () => windowsPlatform,
     });
 
+    testUsingContext('isAtLeastMinimumVersion returns false when the version found is too old', () {
+      setMockCompatibleVisualStudioInstallation(null);
+      setMockPrereleaseVisualStudioInstallation(null);
+      setMockAnyVisualStudioInstallation(_tooOldResponse);
+
+      visualStudio = VisualStudio();
+      expect(visualStudio.isInstalled, true);
+      expect(visualStudio.isAtLeastMinimumVersion, false);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFilesystem,
+      ProcessManager: () => mockProcessManager,
+      Platform: () => windowsPlatform,
+    });
+
     testUsingContext('isComplete returns false when an incomplete installation is found', () {
       setMockCompatibleVisualStudioInstallation(null);
       setMockPrereleaseVisualStudioInstallation(null);
@@ -421,6 +467,7 @@
 
       visualStudio = VisualStudio();
       expect(visualStudio.isInstalled, true);
+      expect(visualStudio.isAtLeastMinimumVersion, true);
       expect(visualStudio.hasNecessaryComponents, true);
       expect(visualStudio.vcvarsPath, equals(vcvarsPath));
     }, overrides: <Type, Generator>{
diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart
index a1bd362..d8ee9d0 100644
--- a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart
+++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart
@@ -21,24 +21,40 @@
       mockVisualStudio = MockVisualStudio();
       // Default values regardless of whether VS is installed or not.
       when(mockVisualStudio.workloadDescription).thenReturn('Desktop development');
-      when(mockVisualStudio.necessaryComponentDescriptions(any)).thenReturn(<String>['A', 'B']);
+      when(mockVisualStudio.minimumVersionDescription).thenReturn('2019');
+      when(mockVisualStudio.necessaryComponentDescriptions()).thenReturn(<String>['A', 'B']);
     });
 
     // Assigns default values for a complete VS installation with necessary components.
     void _configureMockVisualStudioAsInstalled() {
       when(mockVisualStudio.isInstalled).thenReturn(true);
+      when(mockVisualStudio.isAtLeastMinimumVersion).thenReturn(true);
+      when(mockVisualStudio.isPrerelease).thenReturn(false);
+      when(mockVisualStudio.isComplete).thenReturn(true);
+      when(mockVisualStudio.isLaunchable).thenReturn(true);
+      when(mockVisualStudio.isRebootRequired).thenReturn(false);
+      when(mockVisualStudio.hasNecessaryComponents).thenReturn(true);
+      when(mockVisualStudio.fullVersion).thenReturn('16.2');
+      when(mockVisualStudio.displayName).thenReturn('Visual Studio Community 2019');
+    }
+
+    // Assigns default values for a complete VS installation that is too old.
+    void _configureMockVisualStudioAsTooOld() {
+      when(mockVisualStudio.isInstalled).thenReturn(true);
+      when(mockVisualStudio.isAtLeastMinimumVersion).thenReturn(false);
       when(mockVisualStudio.isPrerelease).thenReturn(false);
       when(mockVisualStudio.isComplete).thenReturn(true);
       when(mockVisualStudio.isLaunchable).thenReturn(true);
       when(mockVisualStudio.isRebootRequired).thenReturn(false);
       when(mockVisualStudio.hasNecessaryComponents).thenReturn(true);
       when(mockVisualStudio.fullVersion).thenReturn('15.1');
-      when(mockVisualStudio.displayName).thenReturn('Visual Studio Community 2019');
+      when(mockVisualStudio.displayName).thenReturn('Visual Studio Community 2017');
     }
 
     // Assigns default values for a missing VS installation.
     void _configureMockVisualStudioAsNotInstalled() {
       when(mockVisualStudio.isInstalled).thenReturn(false);
+      when(mockVisualStudio.isAtLeastMinimumVersion).thenReturn(false);
       when(mockVisualStudio.isPrerelease).thenReturn(false);
       when(mockVisualStudio.isComplete).thenReturn(false);
       when(mockVisualStudio.isLaunchable).thenReturn(false);
@@ -97,6 +113,23 @@
       VisualStudio: () => mockVisualStudio,
     });
 
+    testUsingContext('Emits partial status when Visual Studio is installed but too old', () async {
+      _configureMockVisualStudioAsTooOld();
+      const VisualStudioValidator validator = VisualStudioValidator();
+      final ValidationResult result = await validator.validate();
+      final ValidationMessage expectedMessage = ValidationMessage.error(
+        userMessages.visualStudioTooOld(
+          visualStudio.minimumVersionDescription,
+          visualStudio.workloadDescription,
+          visualStudio.necessaryComponentDescriptions(),
+        ),
+      );
+      expect(result.messages.contains(expectedMessage), true);
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      VisualStudio: () => mockVisualStudio,
+    });
+
 
     testUsingContext('Emits partial status when Visual Studio is installed without necessary components', () async {
       _configureMockVisualStudioAsInstalled();
@@ -127,7 +160,7 @@
       final ValidationMessage expectedMessage = ValidationMessage.error(
         userMessages.visualStudioMissing(
           visualStudio.workloadDescription,
-          visualStudio.necessaryComponentDescriptions(validator.majorVersion),
+          visualStudio.necessaryComponentDescriptions(),
         ),
       );
       expect(result.messages.contains(expectedMessage), true);