Workspaces glob support (#4680)

diff --git a/lib/src/language_version.dart b/lib/src/language_version.dart
index 0f5fffa..c00f099 100644
--- a/lib/src/language_version.dart
+++ b/lib/src/language_version.dart
@@ -65,6 +65,8 @@
 
   bool get supportsWorkspaces => this >= firstVersionWithWorkspaces;
 
+  bool get supportsWorkspaceGlobs => this >= firstVersionWithWorkspaceGlobs;
+
   bool get supportsTagPattern => this >= firstVersionWithTagPattern;
 
   bool get forbidsUnknownDescriptionKeys =>
@@ -110,6 +112,7 @@
   static const firstVersionWithNullSafety = LanguageVersion(2, 12);
   static const firstVersionWithShorterHostedSyntax = LanguageVersion(2, 15);
   static const firstVersionWithWorkspaces = LanguageVersion(3, 5);
+  static const firstVersionWithWorkspaceGlobs = LanguageVersion(3, 11);
   static const firstVersionWithTagPattern = LanguageVersion(3, 9);
   static const firstVersionForbidingUnknownDescriptionKeys = LanguageVersion(
     3,
diff --git a/lib/src/package.dart b/lib/src/package.dart
index a92eaec..88658d6 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -4,6 +4,8 @@
 
 import 'dart:io';
 
+import 'package:glob/glob.dart';
+import 'package:glob/list_local_fs.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 
@@ -11,6 +13,7 @@
 import 'git.dart' as git;
 import 'ignore.dart';
 import 'io.dart';
+import 'language_version.dart';
 import 'log.dart' as log;
 import 'package_name.dart';
 import 'pubspec.dart';
@@ -174,21 +177,44 @@
     );
 
     final workspacePackages =
-        pubspec.workspace.map((workspacePath) {
+        pubspec.workspace.expand((workspacePath) {
+          final Glob glob;
           try {
-            return Package.load(
-              p.join(dir, workspacePath),
-              loadPubspec: loadPubspec,
-              withPubspecOverrides: withPubspecOverrides,
+            glob = Glob(
+              pubspec.languageVersion.supportsWorkspaceGlobs
+                  ? workspacePath
+                  : Glob.quote(workspacePath),
             );
-          } on FileException catch (e) {
-            final pubspecPath = p.join(dir, 'pubspec.yaml');
-            throw FileException(
-              '${e.message}\n'
-              'That was included in the workspace of $pubspecPath.',
-              e.path,
+          } on FormatException catch (e) {
+            fail('Failed to parse glob `$workspacePath`. $e');
+          }
+          final packages = <Package>[];
+          for (final globResult in glob.listSync(root: dir)) {
+            final pubspecPath = p.join(globResult.path, 'pubspec.yaml');
+            if (!fileExists(pubspecPath)) continue;
+            packages.add(
+              Package.load(
+                globResult.path,
+                loadPubspec: loadPubspec,
+                withPubspecOverrides: withPubspecOverrides,
+              ),
             );
           }
+          if (packages.isEmpty) {
+            final globHint =
+                !pubspec.languageVersion.supportsWorkspaceGlobs &&
+                        _looksLikeGlob(workspacePath)
+                    ? '''
+\n\nGlob syntax is only supported from language version ${LanguageVersion.firstVersionWithWorkspaceGlobs}.
+Consider changing the language version of ${p.join(dir, 'pubspec.yaml')} to ${LanguageVersion.firstVersionWithWorkspaceGlobs}.
+'''
+                    : '';
+            fail('''
+No workspace packages matching `$workspacePath`.
+That was included in the workspace of `${p.join(dir, 'pubspec.yaml')}`.$globHint
+''');
+          }
+          return packages;
         }).toList();
     for (final package in workspacePackages) {
       if (package.pubspec.resolution != Resolution.workspace) {
@@ -546,3 +572,5 @@
     }
   }
 }
+
+bool _looksLikeGlob(String s) => Glob.quote(s) != s;
diff --git a/pubspec.lock b/pubspec.lock
index c4db465..d10d255 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -122,7 +122,7 @@
     source: hosted
     version: "4.0.0"
   glob:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: glob
       sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
diff --git a/pubspec.yaml b/pubspec.yaml
index 38570c1..6ef8e86 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,6 +12,7 @@
   convert: ^3.1.2
   crypto: ^3.0.6
   frontend_server_client: ^4.0.0
+  glob: ^2.1.3
   graphs: ^2.3.2
   http: ^1.5.0
   http_multi_server: ^3.2.2
diff --git a/test/workspace_test.dart b/test/workspace_test.dart
index ff9fa35..fd060c0 100644
--- a/test/workspace_test.dart
+++ b/test/workspace_test.dart
@@ -392,17 +392,14 @@
         ),
       ]),
     ]).create();
-    final appABPath = p.join(sandbox, appPath, 'a', 'b');
     final aPubspecPath = p.join('.', 'a', 'pubspec.yaml');
-    final pubspecPath = p.join('.', 'pubspec.yaml');
     await pubGet(
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       error: contains(
-        'Could not find a file named "pubspec.yaml" in "$appABPath".\n'
-        'That was included in the workspace of $aPubspecPath.\n'
-        'That was included in the workspace of $pubspecPath.',
+        'No workspace packages matching `b`.\n'
+        'That was included in the workspace of `$aPubspecPath`',
       ),
-      exitCode: NO_INPUT,
+      exitCode: 1,
     );
   });
 
@@ -866,11 +863,11 @@
       await pubGet(
         environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
         warning: allOf(
-          contains('Deleting old lock-file: `.${s}pkgs/a${s}pubspec.lock'),
-          isNot(contains('.${s}pkgs/b${s}pubspec.lock')),
+          contains('Deleting old lock-file: `.${s}pkgs${s}a${s}pubspec.lock'),
+          isNot(contains('.${s}pkgs${s}b${s}pubspec.lock')),
           contains(
             'Deleting old package config: '
-            '`.${s}pkgs/a$s.dart_tool${s}package_config.json`',
+            '`.${s}pkgs${s}a$s.dart_tool${s}package_config.json`',
           ),
           contains('Deleting old lock-file: `.${s}pkgs${s}pubspec.lock'),
           contains(
@@ -954,7 +951,7 @@
       error: '''
 Packages can only be included in the workspace once.
 
-`.${s}pkgs/a/pubspec.yaml` is included twice into the workspace of `.${s}pubspec.yaml`''',
+`.${s}pkgs${s}a${s}pubspec.yaml` is included twice into the workspace of `.${s}pubspec.yaml`''',
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
     );
   });
@@ -1596,7 +1593,7 @@
       error: allOf(
         contains('Cannot override workspace packages'),
         contains(
-          'Package `a` at `.${s}pkgs/a` is overridden in `pubspec.yaml`.',
+          'Package `a` at `.${s}pkgs${s}a` is overridden in `pubspec.yaml`.',
         ),
       ),
     );
@@ -1765,8 +1762,8 @@
       args: ['get', '--example'],
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
-        isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.`.')),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
+        isNot(contains('Got dependencies in `.${s}pkgs${s}b${s}example`.`.')),
       ),
     );
 
@@ -1774,8 +1771,8 @@
       args: ['upgrade', '--example'],
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
-        isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.`.')),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
+        isNot(contains('Got dependencies in `.${s}pkgs${s}b${s}example`.`.')),
       ),
     );
 
@@ -1783,11 +1780,13 @@
       args: ['upgrade', '--example', '--tighten'],
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
         isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.`.')),
       ),
       error: contains(
-        'Running `upgrade --tighten` only in `.`. Run `dart pub upgrade --tighten --directory .${s}pkgs/a${s}example` separately.',
+        'Running `upgrade --tighten` only in `.`. '
+        'Run `dart pub upgrade --tighten '
+        '--directory .${s}pkgs${s}a${s}example` separately.',
       ),
     );
 
@@ -1795,11 +1794,13 @@
       args: ['upgrade', '--example', '--major-versions'],
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
-        isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.')),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
+        isNot(contains('Got dependencies in `.${s}pkgs${s}b${s}example`.')),
       ),
       error: contains(
-        'Running `upgrade --major-versions` only in `.`. Run `dart pub upgrade --major-versions --directory .${s}pkgs/a${s}example` separately.',
+        'Running `upgrade --major-versions` only in `.`. '
+        'Run `dart pub upgrade --major-versions '
+        '--directory .${s}pkgs${s}a${s}example` separately.',
       ),
     );
 
@@ -1808,8 +1809,8 @@
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
         contains('+ foo 1.5.0'),
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
-        isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.')),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
+        isNot(contains('Got dependencies in `.${s}pkgs${s}b${s}example`.')),
       ),
     );
 
@@ -1818,11 +1819,103 @@
       environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
       output: allOf(
         contains('< foo 1.0.0'),
-        contains('Got dependencies in `.${s}pkgs/a${s}example`.'),
-        isNot(contains('Got dependencies in `.${s}pkgs/b${s}example`.`.')),
+        contains('Got dependencies in `.${s}pkgs${s}a${s}example`.'),
+        isNot(contains('Got dependencies in `.${s}pkgs${s}b${s}example`.`.')),
       ),
     );
   });
+
+  test('bad globs are handled gracefully', () async {
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        extras: {
+          'workspace': ['pkgs/{*'],
+        },
+        sdk: '^3.5.0',
+      ),
+    ]).create();
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.11.0'},
+      error: contains('''
+No workspace packages matching `pkgs/{*`.
+That was included in the workspace of `.${s}pubspec.yaml`.
+
+Glob syntax is only supported from language version 3.11.
+Consider changing the language version of .${s}pubspec.yaml to 3.11.'''),
+    );
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        extras: {
+          'workspace': ['pkgs/{*'],
+        },
+        sdk: '^3.11.0',
+      ),
+    ]).create();
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.11.0'},
+      error: contains(
+        'Failed to parse glob `pkgs/{*`. '
+        'Error on line 1, column 8: expected ",".',
+      ),
+    );
+  });
+
+  test(
+    'globs are not resolved in older language versions',
+    () async {
+      await dir(appPath, [
+        libPubspec(
+          'myapp',
+          '1.2.3',
+          extras: {
+            'workspace': ['pkgs/*'],
+          },
+          sdk: '^3.6.0',
+        ),
+        dir('pkgs', [
+          dir('*', [libPubspec('a', '1.1.1', resolutionWorkspace: true)]),
+          dir('b', [libPubspec('b', '1.1.1', resolutionWorkspace: true)]),
+        ]),
+      ]).create();
+      await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.11.0'});
+      await dir(appPath, [
+        packageConfigFile([
+          packageConfigEntry(name: 'myapp', path: '.'),
+          packageConfigEntry(name: 'a', path: 'pkgs/*'),
+        ], generatorVersion: '3.11.0'),
+      ]).validate();
+    },
+    skip: Platform.isWindows, // Cannot create directory named "*" on Windows.
+  );
+
+  test('globs are resolved with newer language versions', () async {
+    await dir(appPath, [
+      libPubspec(
+        'myapp',
+        '1.2.3',
+        extras: {
+          'workspace': ['pkgs/*'],
+        },
+        sdk: '^3.11.0',
+      ),
+      dir('pkgs', [
+        dir('a', [libPubspec('a', '1.1.1', resolutionWorkspace: true)]),
+        dir('b', [libPubspec('b', '1.1.1', resolutionWorkspace: true)]),
+      ]),
+    ]).create();
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.11.0'});
+    await dir(appPath, [
+      packageConfigFile([
+        packageConfigEntry(name: 'myapp', path: '.'),
+        packageConfigEntry(name: 'a', path: 'pkgs/a'),
+        packageConfigEntry(name: 'b', path: 'pkgs/b'),
+      ], generatorVersion: '3.11.0'),
+    ]).validate();
+  });
 }
 
 final s = p.separator;