Repository: dart-lang/pana (dart)

Issue:
Score support for swift package manager in plugins with ios/macos support
Reading:
https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-plugin-authors#how-to-add-swift-package-manager-support-to-an-existing-flutter-plugin

As far as I can tell we need to detect the presence of a `<plugin_name>/<platform>/Package.swift` file. Where platform is respectively `ios` and `macos` for the two platforms, or `darwin` that covers both.

I suggest we do an initial zero-scoring report note as we did for wasm support:

* If the package is an ios plugin:
  * if it has `<plugin_name>/ios/Package.swift` or `<plugin_name>/darwin/Package.swift` file, mark platform support as fully passing
   * otherwise write "in the future this will receive a reduced score, please migrate to Swift package manager (link)"
* Similarly for macos

Pull request description:
Fixes #1402

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c49e835df..204a1d585 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## 0.22.13
 
 - Pass-through for `formatter/page_width` in `analysis_options.yaml`.
+- Detect support for Swift Package Manager for ios/macos plugins.
 - Upgraded `lints` to `^5.0.0`
 
 ## 0.22.12
diff --git a/lib/src/package_context.dart b/lib/src/package_context.dart
index ba0e6a9d1..3361c5d5d 100644
--- a/lib/src/package_context.dart
+++ b/lib/src/package_context.dart
@@ -272,6 +272,7 @@ class PackageContext {
       tagger.flutterPluginTags(tags, explanations);
       tagger.nullSafetyTags(tags, explanations);
       tagger.wasmReadyTag(tags, explanations);
+      tagger.swiftPackageManagerPluginTag(tags, explanations);
       if (currentSdkVersion.major >= 3) {
         tags.add(PanaTags.isDart3Compatible);
       }
diff --git a/lib/src/report/multi_platform.dart b/lib/src/report/multi_platform.dart
index 4a03465e5..9be9ad1f3 100644
--- a/lib/src/report/multi_platform.dart
+++ b/lib/src/report/multi_platform.dart
@@ -118,13 +118,19 @@ Future<ReportSection> multiPlatform(PackageContext context) async {
   }
 
   final wasmSubsection = await _createWasmSubsection(context);
+  final swiftPackageManagerSubsection =
+      await _createSwiftPackageManagerSubSection(context);
 
   return makeSection(
       id: ReportSectionId.platform,
       title: 'Platform support',
       maxPoints: 20,
       basePath: context.packageDir,
-      subsections: [subsection, wasmSubsection],
+      subsections: [
+        subsection,
+        wasmSubsection,
+        if (swiftPackageManagerSubsection != null) swiftPackageManagerSubsection
+      ],
       maxIssues: 20);
 }
 
@@ -178,3 +184,47 @@ Future<Subsection> _createWasmSubsection(PackageContext context) async {
     );
   }
 }
+
+/// Create a subsection for ios and macos plugins, to highlight supported
+/// for swift package manager (or lack there of).
+Future<Subsection?> _createSwiftPackageManagerSubSection(
+    PackageContext context) async {
+  final tr = await context.staticAnalysis;
+  final description = 'Swift Package Manager support';
+
+  if (tr.tags.contains(PanaTags.isSwiftPmPlugin)) {
+    return Subsection(
+      description,
+      [
+        RawParagraph(
+            'This iOS or macOS plugin supports the Swift Package Manager. '
+            'It will be rewarded additional points in a future version of the scoring model.'),
+        RawParagraph('See https://docs.flutter.dev/to/spm for details.'),
+      ],
+      0,
+      0,
+      ReportStatus.passed,
+    );
+  }
+  final explanation = tr.explanations
+      .where((e) => e.tag == PanaTags.isSwiftPmPlugin)
+      .firstOrNull;
+  if (explanation != null) {
+    return Subsection(
+      description,
+      [
+        explanationToIssue(explanation),
+        RawParagraph(
+          'This package for iOS or macOS does not support the Swift Package Manager. '
+          'It will not receive full points in a future version of the scoring model.',
+        ),
+        RawParagraph('See https://docs.flutter.dev/to/spm for details.'),
+      ],
+      0,
+      0,
+      ReportStatus.failed,
+    );
+  }
+  // Don't complain if this is not an ios/macos plugin.
+  return null;
+}
diff --git a/lib/src/tag/pana_tags.dart b/lib/src/tag/pana_tags.dart
index 6dfbe9429..5c159bb12 100644
--- a/lib/src/tag/pana_tags.dart
+++ b/lib/src/tag/pana_tags.dart
@@ -38,6 +38,7 @@ abstract class PanaTags {
   static const hasScreenshot = 'has:screenshot';
   static const isPlugin = 'is:plugin';
   static const isNullSafe = 'is:null-safe';
+  static const isSwiftPmPlugin = 'is:swiftpm-plugin';
 
   /// Package version is compatible with Dart 3.
   static const isDart3Compatible = 'is:dart3-compatible';
diff --git a/lib/src/tag/tagger.dart b/lib/src/tag/tagger.dart
index e1fd3fff4..6f2e6c299 100644
--- a/lib/src/tag/tagger.dart
+++ b/lib/src/tag/tagger.dart
@@ -94,6 +94,7 @@ export '_specs.dart' show Runtime;
 /// Calculates the tags for the package residing in a given directory.
 class Tagger {
   final String packageName;
+  final String packageDir;
   final AnalysisSession _session;
   final PubspecCache _pubspecCache;
   final bool _isBinaryOnly;
@@ -115,14 +116,15 @@ class Tagger {
   final PackageGraph _packageGraph;
 
   Tagger._(
-    this.packageName,
-    this._session,
-    PubspecCache pubspecCache,
-    this._isBinaryOnly,
-    this._usesFlutter,
-    this._topLibraries,
-    this._publicLibraries,
-  )   : _pubspecCache = pubspecCache,
+      this.packageName,
+      this._session,
+      PubspecCache pubspecCache,
+      this._isBinaryOnly,
+      this._usesFlutter,
+      this._topLibraries,
+      this._publicLibraries,
+      this.packageDir)
+      : _pubspecCache = pubspecCache,
         _packageGraph = PackageGraph(pubspecCache);
 
   /// Assumes that `dart pub get` has been run.
@@ -170,15 +172,8 @@ class Tagger {
     final publicLibraries = nonSrcDartFiles
         .map((s) => Uri.parse('package:${pubspec.name}/$s'))
         .toList();
-    return Tagger._(
-      pubspec.name,
-      session,
-      pubspecCache,
-      isBinaryOnly,
-      pubspec.usesFlutter,
-      topLibraries,
-      publicLibraries,
-    );
+    return Tagger._(pubspec.name, session, pubspecCache, isBinaryOnly,
+        pubspec.usesFlutter, topLibraries, publicLibraries, packageDir);
   }
 
   void sdkTags(List<String> tags, List<Explanation> explanations) {
@@ -355,6 +350,52 @@ class Tagger {
     }
   }
 
+  void swiftPackageManagerPluginTag(
+      List<String> tags, List<Explanation> explanations) {
+    if (!_usesFlutter) return;
+    final pubspec = _pubspecCache.pubspecOfPackage(packageName);
+
+    bool pathExists(dynamic m, List<String> path) {
+      dynamic current = m;
+      for (final e in path) {
+        if (current is! Map) return false;
+        if (!current.containsKey(e)) return false;
+        current = current[e];
+      }
+      return true;
+    }
+
+    var isDarwinPlugin = false;
+    var swiftPmSupport = true;
+
+    for (final darwinOs in ['macos', 'ios']) {
+      if (pathExists(
+          pubspec.originalYaml, ['flutter', 'plugin', 'platforms', darwinOs])) {
+        isDarwinPlugin = true;
+        final osDir = pubspec.originalYaml['flutter']?['plugin']?['platforms']
+                    ?[darwinOs]?['sharedDarwinSource'] ==
+                true
+            ? 'darwin'
+            : darwinOs;
+
+        final packageSwiftFile = path.join(osDir, packageName, 'Package.swift');
+        if (!File(path.join(packageDir, packageSwiftFile)).existsSync()) {
+          swiftPmSupport = false;
+          final osName = {'macos': 'macOS', 'ios': 'iOS'}[darwinOs];
+          explanations.add(Explanation(
+              'Package does not support the Swift Package Manager on $osName',
+              '''
+It does not contain `$packageSwiftFile`.
+''',
+              tag: PanaTags.isSwiftPmPlugin));
+        }
+      }
+    }
+    if (isDarwinPlugin && swiftPmSupport) {
+      tags.add(PanaTags.isSwiftPmPlugin);
+    }
+  }
+
   /// Adds tags for the Dart runtimes that this package supports to [tags].
   ///
   /// Adds [Explanation]s to [explanations] for runtimes not supported.


Test patch:
diff --git a/test/goldens/end2end/audio_service-0.18.10.json b/test/goldens/end2end/audio_service-0.18.10.json
index d5bd2c79b..e9d6026f5 100644
--- a/test/goldens/end2end/audio_service-0.18.10.json
+++ b/test/goldens/end2end/audio_service-0.18.10.json
@@ -155,7 +155,7 @@
         "grantedPoints": 20,
         "maxPoints": 20,
         "status": "failed",
-        "summary": "### [*] 20/20 points: Supports 4 of 6 possible platforms (**iOS**, **Android**, **Web**, Windows, **macOS**, Linux)\n\n* ✓ Android\n\n* ✓ iOS\n\n* ✓ macOS\n\n* ✓ Web\n\n\nThese platforms are not supported:\n\n<details>\n<summary>\nPackage does not support platform `Windows`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that declares support for platforms: `Android`, `iOS`, `macOS`, `Web`.\n</details>\n\n<details>\n<summary>\nPackage does not support platform `Linux`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that declares support for platforms: `Android`, `iOS`, `macOS`, `Web`.\n</details>\n\n\nThese issues are present but do not affect the score, because they may not originate in your package:\n\n<details>\n<summary>\nPackage does not support platform `Web`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that imports:\n* `package:flutter_cache_manager/flutter_cache_manager.dart` that imports:\n* `package:flutter_cache_manager/src/storage/cache_info_repositories/cache_info_repositories.dart` that imports:\n* `package:flutter_cache_manager/src/storage/cache_info_repositories/json_cache_info_repository.dart` that imports:\n* `package:path_provider/path_provider.dart` that declares support for platforms: `Android`, `iOS`, `Windows`, `Linux`, `macOS`.\n</details>\n\n### [x] 0/0 points: WASM compatibility\n\n<details>\n<summary>\nPackage not compatible with runtime wasm\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that imports:\n* `package:flutter_cache_manager/flutter_cache_manager.dart` that imports:\n* `package:flutter_cache_manager/src/web/web_helper.dart` that imports:\n* `package:flutter_cache_manager/src/cache_store.dart` that imports:\n* `package:flutter_cache_manager/src/storage/file_system/file_system.dart` that imports:\n* `package:file/file.dart` that imports:\n* `package:file/src/interface.dart` that imports:\n* `package:file/src/io.dart` that imports:\n* `dart:io`\n</details>\n\nThis package is not compatible with runtime `wasm`, and will not be rewarded full points in a future version of the scoring model.\n\nSee https://dart.dev/web/wasm for details.\n"
+        "summary": "### [*] 20/20 points: Supports 4 of 6 possible platforms (**iOS**, **Android**, **Web**, Windows, **macOS**, Linux)\n\n* ✓ Android\n\n* ✓ iOS\n\n* ✓ macOS\n\n* ✓ Web\n\n\nThese platforms are not supported:\n\n<details>\n<summary>\nPackage does not support platform `Windows`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that declares support for platforms: `Android`, `iOS`, `macOS`, `Web`.\n</details>\n\n<details>\n<summary>\nPackage does not support platform `Linux`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that declares support for platforms: `Android`, `iOS`, `macOS`, `Web`.\n</details>\n\n\nThese issues are present but do not affect the score, because they may not originate in your package:\n\n<details>\n<summary>\nPackage does not support platform `Web`.\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that imports:\n* `package:flutter_cache_manager/flutter_cache_manager.dart` that imports:\n* `package:flutter_cache_manager/src/storage/cache_info_repositories/cache_info_repositories.dart` that imports:\n* `package:flutter_cache_manager/src/storage/cache_info_repositories/json_cache_info_repository.dart` that imports:\n* `package:path_provider/path_provider.dart` that declares support for platforms: `Android`, `iOS`, `Windows`, `Linux`, `macOS`.\n</details>\n\n### [x] 0/0 points: WASM compatibility\n\n<details>\n<summary>\nPackage not compatible with runtime wasm\n</summary>\n\nBecause:\n* `package:audio_service/audio_service.dart` that imports:\n* `package:flutter_cache_manager/flutter_cache_manager.dart` that imports:\n* `package:flutter_cache_manager/src/web/web_helper.dart` that imports:\n* `package:flutter_cache_manager/src/cache_store.dart` that imports:\n* `package:flutter_cache_manager/src/storage/file_system/file_system.dart` that imports:\n* `package:file/file.dart` that imports:\n* `package:file/src/interface.dart` that imports:\n* `package:file/src/io.dart` that imports:\n* `dart:io`\n</details>\n\nThis package is not compatible with runtime `wasm`, and will not be rewarded full points in a future version of the scoring model.\n\nSee https://dart.dev/web/wasm for details.\n\n### [x] 0/0 points: Swift Package Manager support\n\n<details>\n<summary>\nPackage does not support the Swift Package Manager on macOS\n</summary>\n\nIt does not contain `macos/audio_service/Package.swift`.\n\n</details>\n\nThis package for iOS or macOS does not support the Swift Package Manager. It will not receive full points in a future version of the scoring model.\n\nSee https://docs.flutter.dev/to/spm for details.\n"
       },
       {
         "id": "analysis",
diff --git a/test/goldens/end2end/audio_service-0.18.10.json_report.md b/test/goldens/end2end/audio_service-0.18.10.json_report.md
index f479a09bf..cff71fc20 100644
--- a/test/goldens/end2end/audio_service-0.18.10.json_report.md
+++ b/test/goldens/end2end/audio_service-0.18.10.json_report.md
@@ -88,6 +88,21 @@ This package is not compatible with runtime `wasm`, and will not be rewarded ful
 
 See https://dart.dev/web/wasm for details.
 
+### [x] 0/0 points: Swift Package Manager support
+
+<details>
+<summary>
+Package does not support the Swift Package Manager on macOS
+</summary>
+
+It does not contain `macos/audio_service/Package.swift`.
+
+</details>
+
+This package for iOS or macOS does not support the Swift Package Manager. It will not receive full points in a future version of the scoring model.
+
+See https://docs.flutter.dev/to/spm for details.
+
 
 ## 50/50 Pass static analysis
 
diff --git a/test/goldens/end2end/url_launcher-6.1.12.json b/test/goldens/end2end/url_launcher-6.1.12.json
index fe039a500..461acfa35 100644
--- a/test/goldens/end2end/url_launcher-6.1.12.json
+++ b/test/goldens/end2end/url_launcher-6.1.12.json
@@ -133,8 +133,8 @@
         "title": "Platform support",
         "grantedPoints": 20,
         "maxPoints": 20,
-        "status": "passed",
-        "summary": "### [*] 20/20 points: Supports 6 of 6 possible platforms (**iOS**, **Android**, **Web**, **Windows**, **macOS**, **Linux**)\n\n* ✓ Android\n\n* ✓ iOS\n\n* ✓ Windows\n\n* ✓ Linux\n\n* ✓ macOS\n\n* ✓ Web\n\n### [*] 0/0 points: WASM compatibility\n\nThis package is compatible with runtime `wasm`, and will be rewarded additional points in a future version of the scoring model.\n\nSee https://dart.dev/web/wasm for details.\n"
+        "status": "failed",
+        "summary": "### [*] 20/20 points: Supports 6 of 6 possible platforms (**iOS**, **Android**, **Web**, **Windows**, **macOS**, **Linux**)\n\n* ✓ Android\n\n* ✓ iOS\n\n* ✓ Windows\n\n* ✓ Linux\n\n* ✓ macOS\n\n* ✓ Web\n\n### [*] 0/0 points: WASM compatibility\n\nThis package is compatible with runtime `wasm`, and will be rewarded additional points in a future version of the scoring model.\n\nSee https://dart.dev/web/wasm for details.\n\n### [x] 0/0 points: Swift Package Manager support\n\n<details>\n<summary>\nPackage does not support the Swift Package Manager on macOS\n</summary>\n\nIt does not contain `macos/url_launcher/Package.swift`.\n\n</details>\n\nThis package for iOS or macOS does not support the Swift Package Manager. It will not receive full points in a future version of the scoring model.\n\nSee https://docs.flutter.dev/to/spm for details.\n"
       },
       {
         "id": "analysis",
diff --git a/test/goldens/end2end/url_launcher-6.1.12.json_report.md b/test/goldens/end2end/url_launcher-6.1.12.json_report.md
index 8e729700d..45f54ceda 100644
--- a/test/goldens/end2end/url_launcher-6.1.12.json_report.md
+++ b/test/goldens/end2end/url_launcher-6.1.12.json_report.md
@@ -52,6 +52,21 @@ This package is compatible with runtime `wasm`, and will be rewarded additional
 
 See https://dart.dev/web/wasm for details.
 
+### [x] 0/0 points: Swift Package Manager support
+
+<details>
+<summary>
+Package does not support the Swift Package Manager on macOS
+</summary>
+
+It does not contain `macos/url_launcher/Package.swift`.
+
+</details>
+
+This package for iOS or macOS does not support the Swift Package Manager. It will not receive full points in a future version of the scoring model.
+
+See https://docs.flutter.dev/to/spm for details.
+
 
 ## 50/50 Pass static analysis
 
diff --git a/test/tag/tag_end2end_test.dart b/test/tag/tag_end2end_test.dart
index 4ceb5c1e8..a23a157d6 100644
--- a/test/tag/tag_end2end_test.dart
+++ b/test/tag/tag_end2end_test.dart
@@ -578,6 +578,81 @@ name: my_package
     });
   });
 
+  group('swift package manager tag', () {
+    test('Works with ios/<package_name>/Package.swift', () async {
+      final descriptor = d.dir('cache', [
+        packageWithPathDeps('my_package', pubspecExtras: {
+          'flutter': {
+            'plugin': {
+              'platforms': {'ios': <String, dynamic>{}}
+            }
+          }
+        }, extraFiles: [
+          d.dir('ios', [
+            d.dir('my_package', [d.file('Package.swift')])
+          ]),
+        ])
+      ]);
+      await descriptor.create();
+      final tagger = Tagger('${descriptor.io.path}/my_package');
+      _expectTagging(tagger.swiftPackageManagerPluginTag,
+          tags: contains('is:swiftpm-plugin'));
+    });
+
+    test('Works with darwin/package_name/Package.swift', () async {
+      final descriptor = d.dir('cache', [
+        packageWithPathDeps('my_package', pubspecExtras: {
+          'flutter': {
+            'plugin': {
+              'platforms': {
+                'ios': <String, dynamic>{'sharedDarwinSource': true},
+                'macos': <String, dynamic>{'sharedDarwinSource': true}
+              }
+            }
+          }
+        }, extraFiles: [
+          d.dir('darwin', [
+            d.dir('my_package', [d.file('Package.swift')])
+          ]),
+        ])
+      ]);
+      await descriptor.create();
+      final tagger = Tagger('${descriptor.io.path}/my_package');
+      _expectTagging(tagger.swiftPackageManagerPluginTag,
+          tags: contains('is:swiftpm-plugin'));
+    });
+
+    test('Fails with the wrong os/package_name/Package.swift', () async {
+      final descriptor = d.dir('cache', [
+        packageWithPathDeps('my_package', pubspecExtras: {
+          'flutter': {
+            'plugin': {
+              'platforms': {'macos': <String, dynamic>{}}
+            }
+          }
+        }, extraFiles: [
+          d.dir('ios', [
+            d.dir('my_package', [d.file('Package.swift')])
+          ]),
+        ])
+      ]);
+      await descriptor.create();
+      final tagger = Tagger('${descriptor.io.path}/my_package');
+      _expectTagging(tagger.swiftPackageManagerPluginTag,
+          tags: isEmpty,
+          explanations: contains(isA<Explanation>()
+              .having((e) => e.tag, 'tag', 'is:swiftpm-plugin')));
+    });
+
+    test('Does not complain about non-plugins', () async {
+      final descriptor = d.dir('cache', [packageWithPathDeps('my_package')]);
+      await descriptor.create();
+      final tagger = Tagger('${descriptor.io.path}/my_package');
+      _expectTagging(tagger.swiftPackageManagerPluginTag,
+          tags: isEmpty, explanations: isEmpty);
+    });
+  });
+
   group('wasm tag', () {
     test('Excluded with dart:js', () async {
       final descriptor = d.dir('cache', [