Migrate xcodeproj to null safety (#80549)

diff --git a/packages/flutter_tools/lib/src/base/version.dart b/packages/flutter_tools/lib/src/base/version.dart
index 255402c..4192f26 100644
--- a/packages/flutter_tools/lib/src/base/version.dart
+++ b/packages/flutter_tools/lib/src/base/version.dart
@@ -21,6 +21,9 @@
     return Version._(major ?? 0, minor ?? 0, patch ?? 0, text);
   }
 
+  /// Public constant constructor when all fields are non-null, without default value fallbacks.
+  const Version.withText(this.major, this.minor, this.patch, this._text);
+
   Version._(this.major, this.minor, this.patch, this._text) {
     if (major < 0) {
       throw ArgumentError('Major version must be non-negative.');
diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart
index 64c54ba..0e4e53b 100644
--- a/packages/flutter_tools/lib/src/globals.dart
+++ b/packages/flutter_tools/lib/src/globals.dart
@@ -14,7 +14,6 @@
 import 'globals_null_migrated.dart' as globals;
 import 'ios/ios_workflow.dart';
 import 'ios/simulators.dart';
-import 'ios/xcodeproj.dart';
 import 'macos/cocoapods.dart';
 import 'macos/cocoapods_validator.dart';
 import 'macos/xcdevice.dart';
@@ -47,7 +46,6 @@
 IOSSimulatorUtils get iosSimulatorUtils => context.get<IOSSimulatorUtils>();
 IOSWorkflow get iosWorkflow => context.get<IOSWorkflow>();
 Xcode get xcode => context.get<Xcode>();
-XcodeProjectInterpreter get xcodeProjectInterpreter => context.get<XcodeProjectInterpreter>();
 
 XCDevice get xcdevice => context.get<XCDevice>();
 
diff --git a/packages/flutter_tools/lib/src/globals_null_migrated.dart b/packages/flutter_tools/lib/src/globals_null_migrated.dart
index 3078554..f7b1de1 100644
--- a/packages/flutter_tools/lib/src/globals_null_migrated.dart
+++ b/packages/flutter_tools/lib/src/globals_null_migrated.dart
@@ -24,6 +24,7 @@
 import 'base/user_messages.dart';
 import 'cache.dart';
 import 'ios/plist_parser.dart';
+import 'ios/xcodeproj.dart';
 import 'persistent_tool_state.dart';
 import 'reporting/reporting.dart';
 import 'version.dart';
@@ -38,6 +39,7 @@
 AndroidSdk? get androidSdk => context.get<AndroidSdk>();
 FlutterVersion get flutterVersion => context.get<FlutterVersion>()!;
 Usage get flutterUsage => context.get<Usage>()!;
+XcodeProjectInterpreter? get xcodeProjectInterpreter => context.get<XcodeProjectInterpreter>();
 
 PersistentToolState? get persistentToolState => PersistentToolState.instance;
 
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 61c259d..ea544e8 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// @dart = 2.8
-
 import 'package:file/memory.dart';
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
@@ -17,6 +15,7 @@
 import '../base/process.dart';
 import '../base/terminal.dart';
 import '../base/utils.dart';
+import '../base/version.dart';
 import '../build_info.dart';
 import '../reporting/reporting.dart';
 
@@ -26,11 +25,11 @@
 /// Interpreter of Xcode projects.
 class XcodeProjectInterpreter {
   factory XcodeProjectInterpreter({
-    @required Platform platform,
-    @required ProcessManager processManager,
-    @required Logger logger,
-    @required FileSystem fileSystem,
-    @required Usage usage,
+    required Platform platform,
+    required ProcessManager processManager,
+    required Logger logger,
+    required FileSystem fileSystem,
+    required Usage usage,
   }) {
     return XcodeProjectInterpreter._(
       platform: platform,
@@ -42,14 +41,12 @@
   }
 
   XcodeProjectInterpreter._({
-    @required Platform platform,
-    @required ProcessManager processManager,
-    @required Logger logger,
-    @required FileSystem fileSystem,
-    @required Usage usage,
-    int majorVersion,
-    int minorVersion,
-    int patchVersion,
+    required Platform platform,
+    required ProcessManager processManager,
+    required Logger logger,
+    required FileSystem fileSystem,
+    required Usage usage,
+    Version? version,
   }) : _platform = platform,
         _fileSystem = fileSystem,
         _logger = logger,
@@ -60,9 +57,7 @@
           platform: platform,
           processManager: processManager,
         ),
-        _majorVersion = majorVersion,
-        _minorVersion = minorVersion,
-        _patchVersion = patchVersion,
+        _version = version,
         _usage = usage;
 
   /// Create an [XcodeProjectInterpreter] for testing.
@@ -70,12 +65,10 @@
   /// Defaults to installed with sufficient version,
   /// a memory file system, fake platform, buffer logger,
   /// test [Usage], and test [Terminal].
-  /// Set [majorVersion] to null to simulate Xcode not being installed.
+  /// Set [version] to null to simulate Xcode not being installed.
   factory XcodeProjectInterpreter.test({
-    @required ProcessManager processManager,
-    int majorVersion = 1000,
-    int minorVersion = 0,
-    int patchVersion = 0,
+    required ProcessManager processManager,
+    Version? version = const Version.withText(1000, 0, 0, '1000.0.0'),
   }) {
     final Platform platform = FakePlatform(
       operatingSystem: 'macos',
@@ -87,9 +80,7 @@
       processManager: processManager,
       usage: TestUsage(),
       logger: BufferLogger.test(),
-      majorVersion: majorVersion,
-      minorVersion: minorVersion,
-      patchVersion: patchVersion,
+      version: version,
     );
   }
 
@@ -116,52 +107,37 @@
         }
         _versionText = result.stdout.trim().replaceAll('\n', ', ');
       }
-      final Match match = _versionRegex.firstMatch(versionText);
+      final Match? match = _versionRegex.firstMatch(versionText!);
       if (match == null) {
         return;
       }
-      final String version = match.group(1);
+      final String version = match.group(1)!;
       final List<String> components = version.split('.');
-      _majorVersion = int.parse(components[0]);
-      _minorVersion = components.length < 2 ? 0 : int.parse(components[1]);
-      _patchVersion = components.length < 3 ? 0 : int.parse(components[2]);
+      final int majorVersion = int.parse(components[0]);
+      final int minorVersion = components.length < 2 ? 0 : int.parse(components[1]);
+      final int patchVersion = components.length < 3 ? 0 : int.parse(components[2]);
+      _version = Version(majorVersion, minorVersion, patchVersion);
     } on ProcessException {
       // Ignored, leave values null.
     }
   }
 
-  bool get isInstalled => majorVersion != null;
+  bool get isInstalled => version != null;
 
-  String _versionText;
-  String get versionText {
+  String? _versionText;
+  String? get versionText {
     if (_versionText == null) {
       _updateVersion();
     }
     return _versionText;
   }
 
-  int _majorVersion;
-  int get majorVersion {
-    if (_majorVersion == null) {
+  Version? _version;
+  Version? get version {
+    if (_version == null) {
       _updateVersion();
     }
-    return _majorVersion;
-  }
-
-  int _minorVersion;
-  int get minorVersion {
-    if (_minorVersion == null) {
-      _updateVersion();
-    }
-    return _minorVersion;
-  }
-
-  int _patchVersion;
-  int get patchVersion {
-    if (_patchVersion == null) {
-      _updateVersion();
-    }
-    return _patchVersion;
+    return _version;
   }
 
   /// The `xcrun` Xcode command to run or locate development
@@ -190,7 +166,7 @@
   /// target (by default this is Runner).
   Future<Map<String, String>> getBuildSettings(
     String projectPath, {
-    String scheme,
+    String? scheme,
     Duration timeout = const Duration(minutes: 1),
   }) async {
     final Status status = _logger.startSpinner();
@@ -247,7 +223,7 @@
     ], workingDirectory: _fileSystem.currentDirectory.path);
   }
 
-  Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
+  Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
     // The exit code returned by 'xcodebuild -list' when either:
     // * -project is passed and the given project isn't there, or
     // * no -project is passed and there isn't a project.
@@ -287,9 +263,9 @@
 
 Map<String, String> parseXcodeBuildSettings(String showBuildSettingsOutput) {
   final Map<String, String> settings = <String, String>{};
-  for (final Match match in showBuildSettingsOutput.split('\n').map<Match>(_settingExpr.firstMatch)) {
+  for (final Match? match in showBuildSettingsOutput.split('\n').map<Match?>(_settingExpr.firstMatch)) {
     if (match != null) {
-      settings[match[1]] = match[2];
+      settings[match[1]!] = match[2]!;
     }
   }
   return settings;
@@ -303,7 +279,7 @@
     return str;
   }
 
-  return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]] ?? m[0]);
+  return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]!] ?? m[0]!);
 }
 
 /// Information about an Xcode project.
@@ -321,7 +297,7 @@
     final List<String> targets = <String>[];
     final List<String> buildConfigurations = <String>[];
     final List<String> schemes = <String>[];
-    List<String> collector;
+    List<String>? collector;
     for (final String line in output.split('\n')) {
       if (line.isEmpty) {
         collector = null;
@@ -353,7 +329,7 @@
 
   /// The expected scheme for [buildInfo].
   @visibleForTesting
-  static String expectedSchemeFor(BuildInfo buildInfo) {
+  static String expectedSchemeFor(BuildInfo? buildInfo) {
     return toTitleCase(buildInfo?.flavor ?? 'runner');
   }
 
@@ -379,7 +355,7 @@
   }
   /// Returns unique scheme matching [buildInfo], or null, if there is no unique
   /// best match.
-  String schemeFor(BuildInfo buildInfo) {
+  String? schemeFor(BuildInfo buildInfo) {
     final String expectedScheme = expectedSchemeFor(buildInfo);
     if (schemes.contains(expectedScheme)) {
       return expectedScheme;
@@ -401,7 +377,7 @@
 
   /// Returns unique build configuration matching [buildInfo] and [scheme], or
   /// null, if there is no unique best match.
-  String buildConfigurationFor(BuildInfo buildInfo, String scheme) {
+  String? buildConfigurationFor(BuildInfo buildInfo, String scheme) {
     final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme);
     if (hasBuildConfigurationForBuildMode(expectedConfiguration)) {
       return expectedConfiguration;
@@ -426,7 +402,7 @@
     return 'Release';
   }
 
-  static String _uniqueMatch(Iterable<String> strings, bool Function(String s) matches) {
+  static String? _uniqueMatch(Iterable<String> strings, bool Function(String s) matches) {
     final List<String> options = strings.where(matches).toList();
     if (options.length == 1) {
       return options.first;
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 9cbf1f8..a225e28 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -13,7 +13,7 @@
 import '../base/project_migrator.dart';
 import '../build_info.dart';
 import '../convert.dart';
-import '../globals.dart' as globals;
+import '../globals_null_migrated.dart' as globals;
 import '../ios/xcode_build_settings.dart';
 import '../ios/xcodeproj.dart';
 import '../project.dart';
diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart
index a734437..493c26d 100644
--- a/packages/flutter_tools/lib/src/macos/xcdevice.dart
+++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart
@@ -18,7 +18,7 @@
 import '../build_info.dart';
 import '../cache.dart';
 import '../convert.dart';
-import '../globals.dart' as globals;
+import '../globals_null_migrated.dart' as globals;
 import '../ios/devices.dart';
 import '../ios/ios_deploy.dart';
 import '../ios/iproxy.dart';
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index d48dca7..530c2a3 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -101,13 +101,7 @@
 
   bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
 
-  Version get currentVersion => Version(
-        _xcodeProjectInterpreter.majorVersion,
-        _xcodeProjectInterpreter.minorVersion,
-        _xcodeProjectInterpreter.patchVersion,
-        text:
-            '${_xcodeProjectInterpreter.majorVersion}.${_xcodeProjectInterpreter.minorVersion}.${_xcodeProjectInterpreter.patchVersion}',
-      );
+  Version get currentVersion => _xcodeProjectInterpreter.version;
 
   String get versionText => _xcodeProjectInterpreter.versionText;
 
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart
index dd28195..04e4954 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/clean_test.dart
@@ -8,6 +8,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/commands/clean.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
@@ -73,7 +74,7 @@
         final FlutterProject projectUnderTest = setupProjectUnderTest(fs.currentDirectory);
         // Xcode is installed and version satisfactory.
         when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(1000);
+        when(mockXcodeProjectInterpreter.version).thenReturn(Version(1000, 0, 0));
         await CleanCommand().runCommand();
 
         expect(buildDirectory.existsSync(), isFalse);
@@ -108,7 +109,7 @@
         setupProjectUnderTest(fs.currentDirectory);
         // Xcode is installed and version satisfactory.
         when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-        when(mockXcodeProjectInterpreter.majorVersion).thenReturn(1000);
+        when(mockXcodeProjectInterpreter.version).thenReturn(Version(1000, 0, 0));
 
         await CleanCommand(verbose: true).runCommand();
         verify(mockXcodeProjectInterpreter.cleanWorkspace(any, 'Runner', verbose: true)).called(2);
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
index aba3dd0..1d75c51 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -10,6 +10,7 @@
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
@@ -92,7 +93,7 @@
 
       mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
       when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-      when(mockXcodeProjectInterpreter.majorVersion).thenReturn(1000);
+      when(mockXcodeProjectInterpreter.version).thenReturn(Version(1000, 0, 0));
       when(mockXcodeProjectInterpreter.xcrunCommand()).thenReturn(<String>['xcrun']);
       when(mockXcodeProjectInterpreter.getInfo(any, projectFilename: anyNamed('projectFilename'))).thenAnswer(
           (_) {
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart
index d8500ff..ec1cc60 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart
@@ -5,6 +5,7 @@
 // @dart = 2.8
 
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
@@ -60,9 +61,7 @@
       processManager: FakeProcessManager.any(),
       xcodeProjectInterpreter: XcodeProjectInterpreter.test(
         processManager: FakeProcessManager.any(),
-        majorVersion: 1000,
-        minorVersion: 0,
-        patchVersion: 0,
+        version: Version(1000, 0, 0)
       ),
     );
 
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
index 508d3d7..f78858c 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -10,6 +10,7 @@
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/ios/xcode_build_settings.dart';
@@ -172,9 +173,7 @@
       ),
     ]);
 
-    expect(xcodeProjectInterpreter.majorVersion, 11);
-    expect(xcodeProjectInterpreter.minorVersion, 4);
-    expect(xcodeProjectInterpreter.patchVersion, 1);
+    expect(xcodeProjectInterpreter.version, Version(11, 4, 1));
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
   });
 
@@ -188,9 +187,7 @@
       ),
     ]);
 
-    expect(xcodeProjectInterpreter.majorVersion, 11);
-    expect(xcodeProjectInterpreter.minorVersion, 0);
-    expect(xcodeProjectInterpreter.patchVersion, 0);
+    expect(xcodeProjectInterpreter.version, Version(11, 0, 0));
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
   });
 
@@ -203,9 +200,7 @@
         stdout: 'Xcode Ultra5000\nBuild version 8E3004b',
       ),
     ]);
-    expect(xcodeProjectInterpreter.majorVersion, isNull);
-    expect(xcodeProjectInterpreter.minorVersion, isNull);
-    expect(xcodeProjectInterpreter.patchVersion, isNull);
+    expect(xcodeProjectInterpreter.version, isNull);
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
   });
 
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
index 9eb1ec0..003b8f4 100644
--- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -10,6 +10,7 @@
 import 'package:flutter_tools/src/base/io.dart' show ProcessException;
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
@@ -137,9 +138,7 @@
 
         testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(9, 0, 0));
 
           expect(xcode.isRequiredVersionSatisfactory, isFalse);
         });
@@ -152,45 +151,35 @@
 
         testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 0, 1));
 
           expect(xcode.isRequiredVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(13);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(13, 0, 0));
 
           expect(xcode.isRequiredVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 3, 0));
 
           expect(xcode.isRequiredVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('xcodeVersionSatisfactory is true when patch version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(2);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 0, 2));
 
           expect(xcode.isRequiredVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('isRecommendedVersionSatisfactory is false when version is less than minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(11, 0, 0));
 
           expect(xcode.isRecommendedVersionSatisfactory, isFalse);
         });
@@ -203,36 +192,28 @@
 
         testWithoutContext('isRecommendedVersionSatisfactory is true when version meets minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 0, 1));
 
           expect(xcode.isRecommendedVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('isRecommendedVersionSatisfactory is true when major version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(13);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(13, 0, 0));
 
           expect(xcode.isRecommendedVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('isRecommendedVersionSatisfactory is true when minor version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 3, 0));
 
           expect(xcode.isRecommendedVersionSatisfactory, isTrue);
         });
 
         testWithoutContext('isRecommendedVersionSatisfactory is true when patch version exceeds minimum', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(2);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 0, 2));
 
           expect(xcode.isRecommendedVersionSatisfactory, isTrue);
         });
@@ -246,9 +227,7 @@
 
         testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(0);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(10, 2, 0));
 
           expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
@@ -256,9 +235,7 @@
 
         testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
           when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
-          when(mockXcodeProjectInterpreter.majorVersion).thenReturn(12);
-          when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
-          when(mockXcodeProjectInterpreter.patchVersion).thenReturn(1);
+          when(mockXcodeProjectInterpreter.version).thenReturn(Version(12, 0, 1));
 
           expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
           expect(fakeProcessManager.hasRemainingExpectations, isFalse);
@@ -346,7 +323,7 @@
           processManager: FakeProcessManager.any(),
           xcodeProjectInterpreter: XcodeProjectInterpreter.test(
             processManager: FakeProcessManager.any(),
-            majorVersion: null, // Not installed.
+            version: null, // Not installed.
           ),
         );
         xcdevice = XCDevice(
diff --git a/packages/flutter_tools/test/general.shard/utils_test.dart b/packages/flutter_tools/test/general.shard/utils_test.dart
index b7d779e..0075f34 100644
--- a/packages/flutter_tools/test/general.shard/utils_test.dart
+++ b/packages/flutter_tools/test/general.shard/utils_test.dart
@@ -26,6 +26,7 @@
     testWithoutContext('can parse and compare', () {
       expect(Version.unknown.toString(), equals('unknown'));
       expect(Version(null, null, null).toString(), equals('0'));
+      expect(const Version.withText(1, 2, 3, 'versionText').toString(), 'versionText');
 
       final Version v1 = Version.parse('1')!;
       expect(v1.major, equals(1));
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 0f106ee..af9f950 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -18,6 +18,7 @@
 import 'package:flutter_tools/src/base/signals.dart';
 import 'package:flutter_tools/src/base/template.dart';
 import 'package:flutter_tools/src/base/terminal.dart';
+import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/doctor_validator.dart';
 import 'package:flutter_tools/src/isolated/mustache_template.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -342,13 +343,7 @@
   String get versionText => 'Xcode 12.0.1';
 
   @override
-  int get majorVersion => 12;
-
-  @override
-  int get minorVersion => 0;
-
-  @override
-  int get patchVersion => 1;
+  Version get version => Version(12, 0, 1);
 
   @override
   Future<Map<String, String>> getBuildSettings(