Only exclude platform directories that actually exist in analysis_options.yaml (#191151)

`AnalysisOptionsMigration` wrote a fixed seven-entry `exclude` list
(`android/**`, `ios/**`, `web/**`, `windows/**`, `macos/**`, `linux/**`,
plus `build/**`) into `analysis_options.yaml` on every `flutter pub get`
/ `flutter analyze` / `flutter run` / `flutter build`, regardless of
what the project actually contains.

Two concrete problems from this:
- A Dart-only package (e.g. `dart create -t web`) that uses `web/` as a
real source directory gets it silently excluded from analysis the first
time any `flutter` command touches the package (this happens in mixed
Flutter/Dart monorepos and Dart workspaces).
- A Flutter app created with `flutter create --platforms=android,ios`
still gets exclusions for `web/`, `windows/`, `macos/`, and `linux/`,
which it doesn't have, and `ProjectMigrator` has no opt-out — every
command restores the full list.

Fix:
- `AnalysisOptionsMigration.migrate()` now skips entirely when the
package has no `flutter` dependency (i.e. it isn't a Flutter project at
all — the discriminator suggested in the issue).
- The exclude list is now built conditionally from
`project.<platform>.existsSync()` for each platform, so only directories
the project actually has as platform scaffolds are excluded. `build/**`
stays unconditional.
- `templates/app/analysis_options.yaml.tmpl` (used by `flutter create`)
gets the same per-platform conditionals via the existing mustache
context (`{{#android}}`, `{{#ios}}`, etc.), so newly created projects
don't get the fixed list either.

Fixes #191131

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/packages/flutter_tools/lib/src/migrations/analysis_options_migration.dart b/packages/flutter_tools/lib/src/migrations/analysis_options_migration.dart
index 35c0abb..2fb42b6 100644
--- a/packages/flutter_tools/lib/src/migrations/analysis_options_migration.dart
+++ b/packages/flutter_tools/lib/src/migrations/analysis_options_migration.dart
@@ -29,6 +29,14 @@
       return;
     }
 
+    // A pure Dart package (no `flutter` dependency) has no platform scaffold
+    // directories, so `web/`, `android/`, etc. are ordinary source or asset
+    // directories rather than generated platform code. Excluding them here
+    // would silently drop them from analysis.
+    if (!_project.manifest.dependencies.contains('flutter')) {
+      return;
+    }
+
     final String originalContent = _analysisOptionsFile.readAsStringSync();
     final YamlNode root;
     try {
@@ -43,14 +51,14 @@
       return;
     }
 
-    const excludesToExclude = <String>[
+    final excludesToExclude = <String>[
       'build/**',
-      'android/**',
-      'ios/**',
-      'web/**',
-      'windows/**',
-      'macos/**',
-      'linux/**',
+      if (_project.android.existsSync()) 'android/**',
+      if (_project.ios.existsSync()) 'ios/**',
+      if (_project.web.existsSync()) 'web/**',
+      if (_project.windows.existsSync()) 'windows/**',
+      if (_project.macos.existsSync()) 'macos/**',
+      if (_project.linux.existsSync()) 'linux/**',
     ];
 
     final Set<String> activeExcludes = await _collectExcludes(_analysisOptionsFile);
diff --git a/packages/flutter_tools/templates/app/analysis_options.yaml.tmpl b/packages/flutter_tools/templates/app/analysis_options.yaml.tmpl
index 775c7ce..86d64ab 100644
--- a/packages/flutter_tools/templates/app/analysis_options.yaml.tmpl
+++ b/packages/flutter_tools/templates/app/analysis_options.yaml.tmpl
@@ -14,12 +14,24 @@
 analyzer:
   exclude:
     - build/**
+{{#android}}
     - android/**
+{{/android}}
+{{#ios}}
     - ios/**
+{{/ios}}
+{{#web}}
     - web/**
+{{/web}}
+{{#windows}}
     - windows/**
+{{/windows}}
+{{#macos}}
     - macos/**
+{{/macos}}
+{{#linux}}
     - linux/**
+{{/linux}}
 {{^withEmptyMain}}
 
 linter:
diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart
index 8aced1d..abec23d 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart
@@ -4209,6 +4209,23 @@
     },
   );
 
+  testUsingContext('analysis_options.yaml only excludes platforms that were generated', () async {
+    await _createProject(
+      projectDir,
+      <String>['--no-pub', '--platforms', 'android,ios'],
+      <String>['analysis_options.yaml'],
+    );
+
+    final String analysisOptions = projectDir.childFile('analysis_options.yaml').readAsStringSync();
+    expect(analysisOptions, contains('- build/**'));
+    expect(analysisOptions, contains('- android/**'));
+    expect(analysisOptions, contains('- ios/**'));
+    expect(analysisOptions, isNot(contains('- web/**')));
+    expect(analysisOptions, isNot(contains('- windows/**')));
+    expect(analysisOptions, isNot(contains('- macos/**')));
+    expect(analysisOptions, isNot(contains('- linux/**')));
+  });
+
   testUsingContext('should escape ":" in project description', () async {
     await _createProject(
       projectDir,
diff --git a/packages/flutter_tools/test/general.shard/migrations/analysis_options_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/analysis_options_migration_test.dart
index 15fea38..f497703 100644
--- a/packages/flutter_tools/test/general.shard/migrations/analysis_options_migration_test.dart
+++ b/packages/flutter_tools/test/general.shard/migrations/analysis_options_migration_test.dart
@@ -13,6 +13,8 @@
 
 import '../../src/common.dart';
 
+const _allPlatforms = <String>['android', 'ios', 'web', 'windows', 'macos', 'linux'];
+
 void main() {
   group('Analysis options migration', () {
     testWithoutContext('skipped if analysis_options.yaml file is missing', () async {
@@ -161,6 +163,47 @@
       expect(migratedContents, contains('- linux/**'));
     });
 
+    testWithoutContext(
+      'skipped entirely for a Dart-only package (no flutter dependency)',
+      () async {
+        final _TestContext context = _createTestContext(
+          isFlutterProject: false,
+          platforms: <String>['web'],
+        );
+        const analysisOptionsContents = '''
+include: package:lints/recommended.yaml
+''';
+        context.analysisOptionsFile.writeAsStringSync(analysisOptionsContents);
+
+        final migration = AnalysisOptionsMigration(context.mockProject, context.testLogger);
+        await migration.migrate();
+
+        expect(context.analysisOptionsFile.readAsStringSync(), analysisOptionsContents);
+        expect(context.testLogger.statusText, isEmpty);
+      },
+    );
+
+    testWithoutContext('only excludes platform directories that actually exist', () async {
+      final _TestContext context = _createTestContext(platforms: <String>['android', 'ios']);
+      const analysisOptionsContents = '''
+include: package:flutter_lints/flutter.yaml
+''';
+
+      context.analysisOptionsFile.writeAsStringSync(analysisOptionsContents);
+
+      final migration = AnalysisOptionsMigration(context.mockProject, context.testLogger);
+      await migration.migrate();
+
+      final String migratedContents = context.analysisOptionsFile.readAsStringSync();
+      expect(migratedContents, contains('- build/**'));
+      expect(migratedContents, contains('- android/**'));
+      expect(migratedContents, contains('- ios/**'));
+      expect(migratedContents, isNot(contains('- web/**')));
+      expect(migratedContents, isNot(contains('- windows/**')));
+      expect(migratedContents, isNot(contains('- macos/**')));
+      expect(migratedContents, isNot(contains('- linux/**')));
+    });
+
     testWithoutContext('migrates and preserves comments inside exclude list', () async {
       final _TestContext context = _createTestContext();
       const analysisOptionsContents = '''
@@ -432,17 +475,63 @@
   MemoryFileSystem memoryFileSystem,
   File analysisOptionsFile,
   BufferLogger testLogger,
-  FakeFlutterProject mockProject,
+  FlutterProject mockProject,
 });
 
-_TestContext _createTestContext() {
+/// Builds a test context backed by a real [FlutterProject] view of an
+/// in-memory directory, so that platform existence checks (`android.existsSync()`,
+/// etc.) reflect the directories actually created here, matching production
+/// behavior instead of being separately mocked.
+_TestContext _createTestContext({
+  bool isFlutterProject = true,
+  List<String> platforms = _allPlatforms,
+}) {
   final memoryFileSystem = MemoryFileSystem.test();
-  final File analysisOptionsFile = memoryFileSystem.file('analysis_options.yaml');
+  final Directory projectDirectory = memoryFileSystem.currentDirectory;
+  final File analysisOptionsFile = projectDirectory.childFile('analysis_options.yaml');
   final testLogger = BufferLogger(
     terminal: Terminal.test(),
     outputPreferences: OutputPreferences.test(),
   );
-  final mockProject = FakeFlutterProject(directory: memoryFileSystem.currentDirectory);
+
+  projectDirectory
+      .childFile('pubspec.yaml')
+      .writeAsStringSync(
+        isFlutterProject
+            ? '''
+name: test_project
+dependencies:
+  flutter:
+    sdk: flutter
+'''
+            : '''
+name: test_project
+''',
+      );
+
+  if (platforms.contains('android')) {
+    projectDirectory.childDirectory('android').createSync(recursive: true);
+  }
+  if (platforms.contains('ios')) {
+    projectDirectory.childDirectory('ios').createSync(recursive: true);
+  }
+  if (platforms.contains('web')) {
+    projectDirectory.childDirectory('web').childFile('index.html').createSync(recursive: true);
+  }
+  if (platforms.contains('windows')) {
+    projectDirectory
+        .childDirectory('windows')
+        .childFile('CMakeLists.txt')
+        .createSync(recursive: true);
+  }
+  if (platforms.contains('macos')) {
+    projectDirectory.childDirectory('macos').createSync(recursive: true);
+  }
+  if (platforms.contains('linux')) {
+    projectDirectory.childDirectory('linux').createSync(recursive: true);
+  }
+
+  final FlutterProject mockProject = FlutterProject.fromDirectoryTest(projectDirectory);
   return (
     memoryFileSystem: memoryFileSystem,
     analysisOptionsFile: analysisOptionsFile,
@@ -451,13 +540,6 @@
   );
 }
 
-class FakeFlutterProject extends Fake implements FlutterProject {
-  FakeFlutterProject({required this.directory});
-
-  @override
-  final Directory directory;
-}
-
 class FakePackageConfig extends Fake implements PackageConfig {
   FakePackageConfig(this._packages);