Reland "[flutter_tools] Removes the need of a no-op plugin implementations #48614" (#49085)

diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 635d0e9..2117656 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -2,13 +2,17 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
+
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/dart/package_map.dart';
 import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/plugins.dart';
 import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 
@@ -23,15 +27,22 @@
     MockMacOSProject macosProject;
     MockAndroidProject androidProject;
     MockWebProject webProject;
+    MockWindowsProject windowsProject;
+    MockLinuxProject linuxProject;
     File packagesFile;
     Directory dummyPackageDirectory;
+    SystemClock mockClock;
+    FlutterVersion mockVersion;
 
     setUp(() async {
       fs = MemoryFileSystem();
+      mockClock = MockClock();
+      mockVersion = MockFlutterVersion();
 
       // Add basic properties to the Flutter project and subprojects
       flutterProject = MockFlutterProject();
       when(flutterProject.directory).thenReturn(fs.directory('/'));
+      // TODO(franciscojma): Remove logic for .flutter-plugins it's deprecated.
       when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins'));
       when(flutterProject.flutterPluginsDependenciesFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins-dependencies'));
       iosProject = MockIosProject();
@@ -39,18 +50,41 @@
       when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner'));
       when(iosProject.podfile).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile'));
       when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock'));
+      when(iosProject.pluginConfigKey).thenReturn('ios');
+      when(iosProject.existsSync()).thenReturn(false);
       macosProject = MockMacOSProject();
       when(flutterProject.macos).thenReturn(macosProject);
       when(macosProject.podfile).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile'));
       when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock'));
+      when(macosProject.pluginConfigKey).thenReturn('macos');
+      when(macosProject.existsSync()).thenReturn(false);
       androidProject = MockAndroidProject();
       when(flutterProject.android).thenReturn(androidProject);
       when(androidProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('android').childDirectory('app'));
       when(androidProject.hostAppGradleRoot).thenReturn(flutterProject.directory.childDirectory('android'));
+      when(androidProject.pluginConfigKey).thenReturn('android');
+      when(androidProject.existsSync()).thenReturn(false);
       webProject = MockWebProject();
       when(flutterProject.web).thenReturn(webProject);
       when(webProject.libDirectory).thenReturn(flutterProject.directory.childDirectory('lib'));
       when(webProject.existsSync()).thenReturn(true);
+      when(webProject.pluginConfigKey).thenReturn('web');
+      when(webProject.existsSync()).thenReturn(false);
+      windowsProject = MockWindowsProject();
+      when(flutterProject.windows).thenReturn(windowsProject);
+      when(windowsProject.pluginConfigKey).thenReturn('windows');
+      when(windowsProject.existsSync()).thenReturn(false);
+      linuxProject = MockLinuxProject();
+      when(flutterProject.linux).thenReturn(linuxProject);
+      when(linuxProject.pluginConfigKey).thenReturn('linux');
+      when(linuxProject.existsSync()).thenReturn(false);
+
+      when(mockClock.now()).thenAnswer(
+        (Invocation _) => DateTime(1970, 1, 1)
+      );
+      when(mockVersion.frameworkVersion).thenAnswer(
+        (Invocation _) => '1.0.0'
+      );
 
       // Set up a simple .packages file for all the tests to use, pointing to one package.
       dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/');
@@ -67,6 +101,18 @@
       platforms:
         ios:
           pluginClass: FLESomePlugin
+        macos:
+          pluginClass: FLESomePlugin
+        windows:
+          pluginClass: FLESomePlugin
+        linux:
+          pluginClass: FLESomePlugin
+        web:
+          pluginClass: SomePlugin
+          fileName: lib/SomeFile.dart
+        android:
+          pluginClass: SomePlugin
+          package: AndroidPackage
   ''');
     }
 
@@ -239,8 +285,8 @@
 
       testUsingContext('Refreshing the plugin list deletes the plugin file when there were plugins but no longer are', () {
         flutterProject.flutterPluginsFile.createSync();
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        flutterProject.flutterPluginsDependenciesFile.createSync();
+
         refreshPluginsList(flutterProject);
         expect(flutterProject.flutterPluginsFile.existsSync(), false);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
@@ -251,8 +297,8 @@
 
       testUsingContext('Refreshing the plugin list creates a plugin directory when there are plugins', () {
         configureDummyPackageAsPlugin();
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        when(iosProject.existsSync()).thenReturn(true);
+
         refreshPluginsList(flutterProject);
         expect(flutterProject.flutterPluginsFile.existsSync(), true);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
@@ -265,11 +311,20 @@
         createPluginWithDependencies(name: 'plugin-a', dependencies: const <String>['plugin-b', 'plugin-c', 'random-package']);
         createPluginWithDependencies(name: 'plugin-b', dependencies: const <String>['plugin-c']);
         createPluginWithDependencies(name: 'plugin-c', dependencies: const <String>[]);
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        when(iosProject.existsSync()).thenReturn(true);
+
+        final DateTime dateCreated = DateTime(1970, 1, 1);
+        when(mockClock.now()).thenAnswer(
+          (Invocation _) => dateCreated
+        );
+        const String version = '1.0.0';
+        when(mockVersion.frameworkVersion).thenAnswer(
+          (Invocation _) => version
+        );
 
         refreshPluginsList(flutterProject);
 
+        // Verify .flutter-plugins-dependencies is configured correctly.
         expect(flutterProject.flutterPluginsFile.existsSync(), true);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
         expect(flutterProject.flutterPluginsFile.readAsStringSync(),
@@ -279,28 +334,79 @@
           'plugin-c=/.tmp_rand0/plugin.rand2/\n'
           ''
         );
-        expect(flutterProject.flutterPluginsDependenciesFile.readAsStringSync(),
-          '{'
-            '"_info":"// This is a generated file; do not edit or check into version control.",'
-            '"dependencyGraph":['
-              '{'
-                '"name":"plugin-a",'
-                '"dependencies":["plugin-b","plugin-c"]'
-              '},'
-              '{'
-                '"name":"plugin-b",'
-                '"dependencies":["plugin-c"]'
-              '},'
-              '{'
-                '"name":"plugin-c",'
-                '"dependencies":[]'
-              '}'
-            ']'
-          '}'
-        );
+
+        final String pluginsString = flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
+        final Map<String, dynamic> jsonContent = json.decode(pluginsString) as  Map<String, dynamic>;
+        expect(jsonContent['info'], 'This is a generated file; do not edit or check into version control.');
+
+        final Map<String, dynamic> plugins = jsonContent['plugins'] as Map<String, dynamic>;
+        final List<dynamic> expectedPlugins = <dynamic>[
+          <String, dynamic> {
+            'name': 'plugin-a',
+            'path': '/.tmp_rand0/plugin.rand0/',
+            'dependencies': <String>[
+              'plugin-b',
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-b',
+            'path': '/.tmp_rand0/plugin.rand1/',
+            'dependencies': <String>[
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-c',
+            'path': '/.tmp_rand0/plugin.rand2/',
+            'dependencies': <String>[]
+          },
+        ];
+        expect(plugins['ios'], expectedPlugins);
+        expect(plugins['android'], expectedPlugins);
+        expect(plugins['macos'], <dynamic>[]);
+        expect(plugins['windows'], <dynamic>[]);
+        expect(plugins['linux'], <dynamic>[]);
+        expect(plugins['web'], <dynamic>[]);
+
+        final List<dynamic> expectedDependencyGraph = <dynamic>[
+          <String, dynamic> {
+            'name': 'plugin-a',
+            'dependencies': <String>[
+              'plugin-b',
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-b',
+            'dependencies': <String>[
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-c',
+            'dependencies': <String>[]
+          },
+        ];
+
+        expect(jsonContent['dependencyGraph'], expectedDependencyGraph);
+        expect(jsonContent['date_created'], dateCreated.toString());
+        expect(jsonContent['version'], version);
+
+        // Make sure tests are updated if a new object is added/removed.
+        final List<String> expectedKeys = <String>[
+          'info',
+          'plugins',
+          'dependencyGraph',
+          'date_created',
+          'version',
+        ];
+        expect(jsonContent.keys, expectedKeys);
       }, overrides: <Type, Generator>{
         FileSystem: () => fs,
         ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
       });
 
       testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () {
@@ -315,6 +421,31 @@
       }, overrides: <Type, Generator>{
         FileSystem: () => fs,
         ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
+      });
+
+      testUsingContext('No changes to the plugin list does not invalidate the Cocoapod lockfiles', () {
+        configureDummyPackageAsPlugin();
+        when(iosProject.existsSync()).thenReturn(true);
+        when(macosProject.existsSync()).thenReturn(true);
+
+        // First call will create the .flutter-plugins-dependencies and the legacy .flutter-plugins file.
+        // Since there was no plugins list, the lock files will be invalidated.
+        // The second call is where the plugins list is compared to the existing one, and if there is no change,
+        // the podfiles shouldn't be invalidated.
+        refreshPluginsList(flutterProject);
+        simulatePodInstallRun(iosProject);
+        simulatePodInstallRun(macosProject);
+
+        refreshPluginsList(flutterProject);
+        expect(iosProject.podManifestLock.existsSync(), true);
+        expect(macosProject.podManifestLock.existsSync(), true);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
       });
     });
 
@@ -600,6 +731,7 @@
       testUsingContext('Registrant for web doesn\'t escape slashes in imports', () async {
         when(flutterProject.isModule).thenReturn(true);
         when(featureFlags.isWebEnabled).thenReturn(true);
+        when(webProject.existsSync()).thenReturn(true);
 
         final Directory webPluginWithNestedFile =
             fs.systemTempDirectory.createTempSync('web_plugin_with_nested');
@@ -648,3 +780,5 @@
 class MockMacOSProject extends Mock implements MacOSProject {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
 class MockWebProject extends Mock implements WebProject {}
+class MockWindowsProject extends Mock implements WindowsProject {}
+class MockLinuxProject extends Mock implements LinuxProject {}