diff --git a/lib/src/package_analyzer.dart b/lib/src/package_analyzer.dart index 90c0b7738..99bd00cb2 100644 --- a/lib/src/package_analyzer.dart +++ b/lib/src/package_analyzer.dart @@ -175,6 +175,7 @@ class PackageAnalyzer { tagger.runtimeTags(tags_, explanations); tagger.flutterPluginTags(tags_, explanations); tagger.nullSafetyTags(tags_, explanations); + tagger.wasmReadyTag(tags_, explanations); // tags are exposed, explanations are ignored // TODO: use a single result object to derive tags + report tags.addAll(tags_); diff --git a/lib/src/report/multi_platform.dart b/lib/src/report/multi_platform.dart index 3cc1d5d4a..78e612cbd 100644 --- a/lib/src/report/multi_platform.dart +++ b/lib/src/report/multi_platform.dart @@ -37,6 +37,7 @@ Future multiPlatform(String packageDir, Pubspec pubspec) async { Subsection scorePlatforms( List tags, List explanations) { + // Scoring and the report only takes these platforms into account. final tagNames = const { PanaTags.platformIos: 'iOS', PanaTags.platformAndroid: 'Android', @@ -45,12 +46,13 @@ Future multiPlatform(String packageDir, Pubspec pubspec) async { PanaTags.platformMacos: 'macOS', PanaTags.platformLinux: 'Linux', }; + final officialTags = tags.where(tagNames.containsKey).toList(); final sdkExplanations = explanations.where((e) => e.tag != null && e.tag!.startsWith('sdk:')); final platformExplanations = explanations .where((e) => e.tag == null || !e.tag!.startsWith('sdk:')); final officialExplanations = platformExplanations.where((e) => - !tags.contains(e.tag) && + !officialTags.contains(e.tag) && (e.tag == null || tagNames.containsKey(e.tag))); final trustExplanations = explanations.where((e) => tags.contains(e.tag)); final paragraphs = [ @@ -59,7 +61,7 @@ Future multiPlatform(String packageDir, Pubspec pubspec) async { if (sdkExplanations.isNotEmpty) // This empty line is required for `package:markdown` to render the following list correctly. RawParagraph(''), - for (final tag in tags.where((e) => e.startsWith('platform'))) + for (final tag in officialTags.where((e) => e.startsWith('platform'))) RawParagraph('* ✓ ${tagNames[tag]}'), if (officialExplanations.isNotEmpty) RawParagraph('\nThese platforms are not supported:\n'), @@ -70,8 +72,6 @@ Future multiPlatform(String packageDir, Pubspec pubspec) async { ...trustExplanations.map(explanationToIssue), ]; - final officialTags = tags.where(tagNames.containsKey).toList(); - final status = officialTags.where((tag) => tag.startsWith('platform:')).isEmpty ? ReportStatus.failed diff --git a/lib/src/tag/_specs.dart b/lib/src/tag/_specs.dart index 9ddf2a20b..d30bb9d93 100644 --- a/lib/src/tag/_specs.dart +++ b/lib/src/tag/_specs.dart @@ -90,6 +90,12 @@ class Runtime { tag: PanaTags.runtimeWeb, ); + static final wasm = Runtime( + 'wasm', + {..._onAllPlatforms, 'ui', 'ui_web', 'js_interop', 'js_interop_unsafe'}, + tag: PanaTags.isWasmReady, + ); + static final flutterNative = Runtime( 'flutter-native', { diff --git a/lib/src/tag/pana_tags.dart b/lib/src/tag/pana_tags.dart index 21f23891e..79357e371 100644 --- a/lib/src/tag/pana_tags.dart +++ b/lib/src/tag/pana_tags.dart @@ -17,6 +17,7 @@ abstract class PanaTags { static const runtimeFlutterNative = 'runtime:flutter-native'; static const runtimeFlutterWeb = 'runtime:flutter-web'; static const runtimeWeb = 'runtime:web'; + static const isWasmReady = 'is:wasm-ready'; // platform tags static const platformAndroid = 'platform:android'; diff --git a/lib/src/tag/tagger.dart b/lib/src/tag/tagger.dart index 2b66e9b04..3366cd6ae 100644 --- a/lib/src/tag/tagger.dart +++ b/lib/src/tag/tagger.dart @@ -335,6 +335,30 @@ class Tagger { } } + /// Adds the is:wasm-ready tag if there are no uses of disallowed dart: libraries. + void wasmReadyTag(List tags, List explanations) { + final runtime = Runtime.wasm; + final finder = runtimeViolationFinder( + LibraryGraph(_session, runtime.declaredVariables), + runtime, + (List path) => Explanation( + 'Package not compatible with runtime ${runtime.name}', + 'Because:\n${LibraryGraph.formatPath(path)}', + tag: runtime.tag)); + var supports = true; + for (final lib in _topLibraries) { + final violationResult = finder.findViolation(lib); + if (violationResult != null) { + explanations.add(violationResult); + supports = false; + break; + } + } + if (supports) { + tags.add(runtime.tag); + } + } + /// Adds tags for the Dart runtimes that this package supports to [tags]. /// /// Adds [Explanation]s to [explanations] for runtimes not supported. diff --git a/test/goldens/end2end/async-2.11.0.json b/test/goldens/end2end/async-2.11.0.json index c7ab7a041..560e1a8a2 100644 --- a/test/goldens/end2end/async-2.11.0.json +++ b/test/goldens/end2end/async-2.11.0.json @@ -57,6 +57,7 @@ "runtime:native-jit", "runtime:web", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:bsd-3-clause", "license:fsf-libre", diff --git a/test/goldens/end2end/http-0.13.0.json b/test/goldens/end2end/http-0.13.0.json index 5667003d0..fe93d58b3 100644 --- a/test/goldens/end2end/http-0.13.0.json +++ b/test/goldens/end2end/http-0.13.0.json @@ -62,6 +62,7 @@ "runtime:native-jit", "runtime:web", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:bsd-3-clause", "license:fsf-libre", diff --git a/test/goldens/end2end/lints-1.0.0.json b/test/goldens/end2end/lints-1.0.0.json index 89f0dec80..6a8ae3459 100644 --- a/test/goldens/end2end/lints-1.0.0.json +++ b/test/goldens/end2end/lints-1.0.0.json @@ -39,6 +39,7 @@ "runtime:native-jit", "runtime:web", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:bsd-3-clause", "license:fsf-libre", diff --git a/test/goldens/end2end/nsd_android-1.2.2.json b/test/goldens/end2end/nsd_android-1.2.2.json index 493d73db3..687b42ec5 100644 --- a/test/goldens/end2end/nsd_android-1.2.2.json +++ b/test/goldens/end2end/nsd_android-1.2.2.json @@ -58,6 +58,7 @@ "platform:android", "is:plugin", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:mit", "license:fsf-libre", diff --git a/test/goldens/end2end/onepub-1.1.0.json b/test/goldens/end2end/onepub-1.1.0.json index e4fa0ad1d..64fccf781 100644 --- a/test/goldens/end2end/onepub-1.1.0.json +++ b/test/goldens/end2end/onepub-1.1.0.json @@ -111,6 +111,7 @@ "runtime:native-aot", "runtime:native-jit", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:unknown" ], diff --git a/test/goldens/end2end/url_launcher-6.1.12.json b/test/goldens/end2end/url_launcher-6.1.12.json index 09ba911f8..bac6b8ef5 100644 --- a/test/goldens/end2end/url_launcher-6.1.12.json +++ b/test/goldens/end2end/url_launcher-6.1.12.json @@ -104,6 +104,7 @@ "platform:web", "is:plugin", "is:null-safe", + "is:wasm-ready", "is:dart3-compatible", "license:bsd-3-clause", "license:fsf-libre", diff --git a/test/tag/tag_end2end_test.dart b/test/tag/tag_end2end_test.dart index 16e1a3de2..4ceb5c1e8 100644 --- a/test/tag/tag_end2end_test.dart +++ b/test/tag/tag_end2end_test.dart @@ -310,8 +310,11 @@ int fourtyTwo() => 42; _expectTagging(tagger.sdkTags, tags: {'sdk:flutter', 'sdk:dart'}); _expectTagging(tagger.platformTags, tags: {'platform:windows', 'platform:android'}); - _expectTagging(tagger.runtimeTags, - tags: ['runtime:native-aot', 'runtime:native-jit', 'runtime:web']); + _expectTagging(tagger.runtimeTags, tags: [ + 'runtime:native-aot', + 'runtime:native-jit', + 'runtime:web', + ]); _expectTagging(tagger.flutterPluginTags, tags: isEmpty); }); @@ -406,7 +409,7 @@ Because: finding: 'Package not compatible with runtime js', explanation: ''' Because: * `package:my_package/my_package.dart` that imports: -* `dart:io`''') +* `dart:io`'''), }); _expectTagging(tagger.flutterPluginTags, tags: isEmpty); }); @@ -449,7 +452,11 @@ int fourtyThree() => 43; }, explanations: isEmpty); _expectTagging(tagger.runtimeTags, - tags: {'runtime:native-aot', 'runtime:native-jit', 'runtime:web'}, + tags: { + 'runtime:native-aot', + 'runtime:native-jit', + 'runtime:web', + }, explanations: isEmpty); _expectTagging(tagger.flutterPluginTags, tags: isEmpty); }); @@ -570,6 +577,71 @@ name: my_package _expectTagging(tagger.flutterPluginTags, tags: {'is:plugin'}); }); }); + + group('wasm tag', () { + test('Excluded with dart:js', () async { + final descriptor = d.dir('cache', [ + packageWithPathDeps('my_package', lib: [ + d.file('my_package.dart', ''' +import 'dart:js'; +'''), + ]) + ]); + + await descriptor.create(); + final tagger = Tagger('${descriptor.io.path}/my_package'); + _expectTagging(tagger.wasmReadyTag, + tags: isNot(contains('is:wasm-ready'))); + }); + + test('Excluded with dart:js_util', () async { + final descriptor = d.dir('cache', [ + packageWithPathDeps('my_package', lib: [ + d.file('my_package.dart', ''' +import 'dart:js_util'; +'''), + ]) + ]); + + await descriptor.create(); + final tagger = Tagger('${descriptor.io.path}/my_package'); + _expectTagging(tagger.wasmReadyTag, + tags: isNot(contains('is:wasm-ready'))); + }); + test('Excluded with dart:html', () async { + final descriptor = d.dir('cache', [ + packageWithPathDeps('my_package', lib: [ + d.file('my_package.dart', ''' +import 'dart:html'; +'''), + ]) + ]); + + await descriptor.create(); + final tagger = Tagger('${descriptor.io.path}/my_package'); + _expectTagging(tagger.wasmReadyTag, + tags: isNot(contains('is:wasm-ready'))); + }); + + test( + 'Included with dart:ui, dart:ui_web dart:js_interop dart:js_interop_unsafe', + () async { + final descriptor = d.dir('cache', [ + packageWithPathDeps('my_package', lib: [ + d.file('my_package.dart', ''' +import 'dart:ui'; +import 'dart:ui_web'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; +'''), + ]) + ]); + + await descriptor.create(); + final tagger = Tagger('${descriptor.io.path}/my_package'); + _expectTagging(tagger.wasmReadyTag, tags: contains('is:wasm-ready')); + }); + }); } Matcher _explanation(