associate analysis options with `FileState` instances

Preliminary to https://github.com/dart-lang/sdk/issues/54124.

Next step will be to update all lookups to use this new getter.


Change-Id: Ib4a0234ffc8badf906eee4fbd8c363ef4a25adac
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/345745
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
diff --git a/pkg/analyzer/lib/src/dart/analysis/context_builder.dart b/pkg/analyzer/lib/src/dart/analysis/context_builder.dart
index 7995df1..e6e52fc 100644
--- a/pkg/analyzer/lib/src/dart/analysis/context_builder.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/context_builder.dart
@@ -204,8 +204,9 @@
       map.add(contextRoot.root, options);
     } else {
       for (var entry in optionsMappings) {
-        var options = AnalysisOptionsImpl();
-        var optionsYaml = provider.getOptionsFromFile(entry.value);
+        var file = entry.value;
+        var options = AnalysisOptionsImpl(file: file);
+        var optionsYaml = provider.getOptionsFromFile(file);
         options.applyOptions(optionsYaml);
         updateOptions(options);
         map.add(entry.key, options);
@@ -294,9 +295,9 @@
     ContextRoot contextRoot,
     SourceFactory sourceFactory,
   ) {
-    var options = AnalysisOptionsImpl();
-
     var optionsFile = contextRoot.optionsFile;
+    var options = AnalysisOptionsImpl(file: optionsFile);
+
     if (optionsFile != null) {
       try {
         var provider = AnalysisOptionsProvider(sourceFactory);
diff --git a/pkg/analyzer/lib/src/dart/analysis/file_state.dart b/pkg/analyzer/lib/src/dart/analysis/file_state.dart
index 838bb2b..47f7e66 100644
--- a/pkg/analyzer/lib/src/dart/analysis/file_state.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/file_state.dart
@@ -375,6 +375,9 @@
 class FileState {
   final FileSystemState _fsState;
 
+  /// The [AnalysisOptions] associated with this file.
+  final AnalysisOptionsImpl? analysisOptions;
+
   /// The absolute path of the file.
   final String path;
 
@@ -438,6 +441,7 @@
     this.workspacePackage,
     this._featureSet,
     this.packageLanguageVersion,
+    this.analysisOptions,
   ) : uriProperties = FileUriProperties(uri);
 
   /// The unlinked API signature of the file.
@@ -1497,7 +1501,7 @@
     Version packageLanguageVersion =
         _getLanguageVersion(path, uri, workspacePackage, analysisOptions);
     var file = FileState._(this, path, uri, uriSource, workspacePackage,
-        featureSet, packageLanguageVersion);
+        featureSet, packageLanguageVersion, analysisOptions);
     _pathToFile[path] = file;
     _uriToFile[uri] = file;
     knownFilePaths.add(path);
diff --git a/pkg/analyzer/lib/src/generated/engine.dart b/pkg/analyzer/lib/src/generated/engine.dart
index 89643c1..79cfb36 100644
--- a/pkg/analyzer/lib/src/generated/engine.dart
+++ b/pkg/analyzer/lib/src/generated/engine.dart
@@ -205,6 +205,9 @@
   /// A list of exclude patterns used to exclude some sources from analysis.
   List<String>? _excludePatterns;
 
+  /// The associated `analysis_options.yaml` file (or `null` if there is none).
+  File? file;
+
   @override
   bool lint = false;
 
@@ -246,7 +249,7 @@
 
   /// Initialize a newly created set of analysis options to have their default
   /// values.
-  AnalysisOptionsImpl() {
+  AnalysisOptionsImpl({this.file}) {
     codeStyleOptions = CodeStyleOptionsImpl(this, useFormatter: false);
   }
 
@@ -262,6 +265,7 @@
     warning = options.warning;
     lintRules = options.lintRules;
     if (options is AnalysisOptionsImpl) {
+      file = options.file;
       enableTiming = options.enableTiming;
       propagateLinterExceptions = options.propagateLinterExceptions;
       strictInference = options.strictInference;
diff --git a/pkg/analyzer/test/src/dart/analysis/analysis_context_collection_test.dart b/pkg/analyzer/test/src/dart/analysis/analysis_context_collection_test.dart
index f32ba62..11cc270 100644
--- a/pkg/analyzer/test/src/dart/analysis/analysis_context_collection_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/analysis_context_collection_test.dart
@@ -6,6 +6,7 @@
 import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
 import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
 import 'package:analyzer/src/dart/analysis/file_state.dart';
+import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/test_utilities/mock_sdk.dart';
 import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
@@ -275,21 +276,24 @@
     _assertWorkspaceCollectionText(workspaceRootPath, r'''
 contexts
   /home/test
-    optionsFile: /home/test/analysis_options.yaml
     packagesFile: /home/test/.dart_tool/package_config.json
     workspace: workspace_0
     analyzedFiles
       /home/test/lib/a.dart
         uri: package:test/a.dart
+        analysisOptions_0
         workspacePackage_0_0
   /home/test/lib/nested
-    optionsFile: /home/test/lib/nested/analysis_options.yaml
     packagesFile: /home/test/.dart_tool/package_config.json
     workspace: workspace_1
     analyzedFiles
       /home/test/lib/nested/b.dart
         uri: package:test/nested/b.dart
+        analysisOptions_1
         workspacePackage_1_0
+analysisOptions
+  analysisOptions_0: /home/test/analysis_options.yaml
+  analysisOptions_1: /home/test/lib/nested/analysis_options.yaml
 workspaces
   workspace_0: PubWorkspace
     root: /home/test
@@ -416,13 +420,15 @@
     _assertWorkspaceCollectionText(workspaceRootPath, r'''
 contexts
   /home/test
-    optionsFile: /home/test/analysis_options.yaml
     packagesFile: /home/test/.dart_tool/package_config.json
     workspace: workspace_0
     analyzedFiles
       /home/test/lib/a.dart
         uri: package:test/a.dart
+        analysisOptions_0
         workspacePackage_0_0
+analysisOptions
+  analysisOptions_0: /home/test/analysis_options.yaml
 workspaces
   workspace_0: PubWorkspace
     root: /home/test
@@ -432,6 +438,70 @@
 ''');
   }
 
+  test_pubWorkspace_singleAnalysisOptions_multipleContexts() async {
+    final workspaceRootPath = '/home';
+    final testPackageRootPath = '$workspaceRootPath/test';
+    final testPackageLibPath = '$testPackageRootPath/lib';
+
+    newAnalysisOptionsYamlFile(testPackageRootPath, '');
+
+    newPubspecYamlFile(testPackageRootPath, r'''
+name: test
+''');
+
+    newSinglePackageConfigJsonFile(
+      packagePath: testPackageRootPath,
+      name: 'test',
+    );
+
+    newFile('$testPackageLibPath/a.dart', '');
+
+    final nestedPackageRootPath = '$testPackageRootPath/nested';
+    newPubspecYamlFile(nestedPackageRootPath, r'''
+name: nested
+''');
+    newSinglePackageConfigJsonFile(
+      packagePath: nestedPackageRootPath,
+      name: 'nested',
+    );
+    newFile('$nestedPackageRootPath/lib/b.dart', '');
+
+    // TODO(pq): there should only be one shared options instance
+    _assertWorkspaceCollectionText(workspaceRootPath, r'''
+contexts
+  /home/test
+    packagesFile: /home/test/.dart_tool/package_config.json
+    workspace: workspace_0
+    analyzedFiles
+      /home/test/lib/a.dart
+        uri: package:test/a.dart
+        analysisOptions_0
+        workspacePackage_0_0
+  /home/test/nested
+    packagesFile: /home/test/nested/.dart_tool/package_config.json
+    workspace: workspace_1
+    analyzedFiles
+      /home/test/nested/lib/b.dart
+        uri: package:nested/b.dart
+        analysisOptions_1
+        workspacePackage_1_0
+analysisOptions
+  analysisOptions_0: /home/test/analysis_options.yaml
+  analysisOptions_1: /home/test/analysis_options.yaml
+workspaces
+  workspace_0: PubWorkspace
+    root: /home/test
+    pubPackages
+      workspacePackage_0_0: PubWorkspacePackage
+        root: /home/test
+  workspace_1: PubWorkspace
+    root: /home/test/nested
+    pubPackages
+      workspacePackage_1_0: PubWorkspacePackage
+        root: /home/test/nested
+''');
+  }
+
   void _assertCollectionText(
     AnalysisContextCollectionImpl collection,
     String expected,
@@ -480,6 +550,7 @@
   final ResourceProvider resourceProvider;
   final TreeStringSink sink;
 
+  final Map<AnalysisOptionsImpl, String> _analysisOptionsFiles = Map.identity();
   final Map<Workspace, (int, String)> _workspaces = Map.identity();
   final Map<Workspace, Map<WorkspacePackage, String>> _workspacePackages =
       Map.identity();
@@ -498,12 +569,25 @@
     );
 
     sink.writeElements(
+      'analysisOptions',
+      contextCollection.contexts
+          .expand((c) => c.allAnalysisOptions
+              .where((o) => o is AnalysisOptionsImpl && o.file != null))
+          .toList(),
+      _writeAnalysisOptions,
+    );
+
+    sink.writeElements(
       'workspaces',
       _workspaces.keys.toList(),
       _writeWorkspace,
     );
   }
 
+  String _idOfAnalysisOptions(AnalysisOptionsImpl analysisOptions) {
+    return _indexIdOfAnalysisOptions(analysisOptions);
+  }
+
   String _idOfWorkspace(Workspace workspace) {
     return _indexIdOfWorkspace(workspace).$2;
   }
@@ -520,6 +604,17 @@
     }
   }
 
+  String _indexIdOfAnalysisOptions(AnalysisOptionsImpl analysisOptions) {
+    if (_analysisOptionsFiles[analysisOptions] case final existing?) {
+      return existing;
+    }
+
+    final index = _analysisOptionsFiles.length;
+    final id = 'analysisOptions_$index';
+    _analysisOptionsFiles[analysisOptions] = id;
+    return id;
+  }
+
   (int, String) _indexIdOfWorkspace(Workspace workspace) {
     if (_workspaces[workspace] case final existing?) {
       return existing;
@@ -545,7 +640,6 @@
 
     sink.writelnWithIndent(contextRoot.root.posixPath);
     sink.withIndent(() {
-      _writeNamedFile('optionsFile', contextRoot.optionsFile);
       _writeNamedFile('packagesFile', contextRoot.packagesFile);
       sink.writelnWithIndent(
         'workspace: ${_idOfWorkspace(contextRoot.workspace)}',
@@ -559,12 +653,25 @@
     });
   }
 
+  void _writeAnalysisOptions(AnalysisOptions analysisOptions) {
+    final file = (analysisOptions as AnalysisOptionsImpl).file!;
+    _writeNamedFile(_idOfAnalysisOptions(analysisOptions), file);
+  }
+
   void _writeDartFile(FileSystemState fsState, File file) {
     sink.writelnWithIndent(file.posixPath);
     sink.withIndent(() {
       final fileState = fsState.getFileForPath(file.path);
       sink.writelnWithIndent('uri: ${fileState.uri}');
 
+      final analysisOptions = fileState.analysisOptions;
+      if (analysisOptions != null) {
+        if (analysisOptions.file != null) {
+          final id = _idOfAnalysisOptions(analysisOptions);
+          sink.writelnWithIndent(id);
+        }
+      }
+
       final workspacePackage = fileState.workspacePackage;
       if (workspacePackage != null) {
         final id = _idOfWorkspacePackage(workspacePackage);