analyzer: Rewrite all plugin paths to be absolute paths

Fixes https://github.com/dart-lang/sdk/issues/61477

Change-Id: I9f241d91b7a5d61e6772f18da7a399d19d344e6f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/449065
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analyzer/lib/src/analysis_options/analysis_options_provider.dart b/pkg/analyzer/lib/src/analysis_options/analysis_options_provider.dart
index 81cc5ff..78dfcd5 100644
--- a/pkg/analyzer/lib/src/analysis_options/analysis_options_provider.dart
+++ b/pkg/analyzer/lib/src/analysis_options/analysis_options_provider.dart
@@ -9,6 +9,7 @@
 import 'package:analyzer/src/generated/source.dart' show SourceFactory;
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:analyzer/src/util/yaml.dart';
+import 'package:path/path.dart' as path;
 import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
 
@@ -59,7 +60,7 @@
   /// Returns an empty options map if the file does not exist or cannot be
   /// parsed.
   YamlMap getOptionsFromFile(File file) {
-    return getOptionsFromSource(FileSource(file));
+    return getOptionsFromSource(FileSource(file), file.provider.pathContext);
   }
 
   /// Provides the options found in [source].
@@ -67,7 +68,11 @@
   /// Recursively merges options referenced by any `include` directives and
   /// removes any `include` directives from the resulting options map. Returns
   /// an empty options map if the file does not exist or cannot be parsed.
-  YamlMap getOptionsFromSource(Source source, {Set<Source>? handled}) {
+  YamlMap getOptionsFromSource(
+    Source source,
+    path.Context pathContext, {
+    Set<Source>? handled,
+  }) {
     handled ??= {};
     try {
       var options = getOptionsFromString(_readAnalysisOptions(source));
@@ -85,16 +90,23 @@
               .toList(),
         _ => <String>[],
       };
-      var includeOptions = includes.fold(YamlMap(), (options, path) {
-        var includeUri = _sourceFactory.resolveUri(source, path);
-        if (includeUri == null || !handled!.add(includeUri)) {
+      var includeOptions = includes.fold(YamlMap(), (currentOptions, path) {
+        var includeSource = _sourceFactory.resolveUri(source, path);
+        if (includeSource == null || !handled!.add(includeSource)) {
           // Return the existing options, unchanged.
-          return options;
+          return currentOptions;
         }
-        return merge(
-          options,
-          getOptionsFromSource(includeUri, handled: handled),
+        var includedOptions = getOptionsFromSource(
+          includeSource,
+          pathContext,
+          handled: handled,
         );
+        includedOptions = _rewriteRelativePaths(
+          includedOptions,
+          pathContext.dirname(includeSource.fullName),
+          pathContext,
+        );
+        return merge(currentOptions, includedOptions);
       });
       options = merge(includeOptions, options);
       return options;
@@ -147,6 +159,39 @@
       return null;
     }
   }
+
+  /// Walks [options] with semantic knowledge about where paths may appear in an
+  /// analysis options file, rewriting relative paths (relative to [directory])
+  /// as absolute paths.
+  ///
+  /// Namely: paths to plugins which are specified by path.
+  // TODO(srawlins): I think 'exclude' paths should be made absolute too; I
+  // believe there is an existing bug about 'include'd 'exclude' paths.
+  YamlMap _rewriteRelativePaths(
+    YamlMap options,
+    String directory,
+    path.Context pathContext,
+  ) {
+    var pluginsSection = options.valueAt('plugins');
+    if (pluginsSection is! YamlMap) return options;
+    var plugins = <String, Object>{};
+    pluginsSection.nodes.forEach((key, value) {
+      if (key is YamlScalar && value is YamlMap) {
+        var pathValue = value.valueAt('path')?.value;
+        if (pathValue is String) {
+          if (pathContext.isRelative(pathValue)) {
+            // We need to store the absolute path, before this value is used in
+            // a synthetic pub package.
+            pathValue = pathContext.join(directory, pathValue);
+            pathValue = pathContext.normalize(pathValue);
+          }
+
+          plugins[key.value as String] = {'path': pathValue};
+        }
+      }
+    });
+    return merge(options, YamlMap.wrap({'plugins': plugins}));
+  }
 }
 
 /// Thrown on options format exceptions.
diff --git a/pkg/analyzer/lib/src/dart/micro/resolve_file.dart b/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
index 6dff455..5d41daa 100644
--- a/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
+++ b/pkg/analyzer/lib/src/dart/micro/resolve_file.dart
@@ -828,7 +828,10 @@
         performance.run('getOptionsFromFile', (_) {
           try {
             var optionsProvider = AnalysisOptionsProvider(sourceFactory);
-            optionMap = optionsProvider.getOptionsFromSource(source);
+            optionMap = optionsProvider.getOptionsFromSource(
+              source,
+              resourceProvider.pathContext,
+            );
           } catch (_) {}
         });
       }
diff --git a/pkg/analyzer/test/src/options/options_provider_test.dart b/pkg/analyzer/test/src/options/options_provider_test.dart
index e9ad9f7..bf0410e 100644
--- a/pkg/analyzer/test/src/options/options_provider_test.dart
+++ b/pkg/analyzer/test/src/options/options_provider_test.dart
@@ -361,9 +361,37 @@
     expect(options.lintRules, isNot(contains(topLevelLint)));
   }
 
+  test_include_plugins() {
+    newFile('/project/analysis_options.yaml', '''
+plugins:
+  plugin_one:
+    path: foo/bar
+''');
+    newFile('/project/foo/analysis_options.yaml', r'''
+include: ../analysis_options.yaml
+''');
+
+    var options = _getOptionsObject('/project/foo') as AnalysisOptionsImpl;
+
+    expect(options.pluginsOptions.configurations, hasLength(1));
+    var pluginConfiguration = options.pluginsOptions.configurations.first;
+    expect(
+      pluginConfiguration.source,
+      isA<PathPluginSource>().having(
+        (e) => e.toYaml(name: 'plugin_one'),
+        'toYaml',
+        '''
+  plugin_one:
+    path: ${convertPath('/project/foo/bar')}
+''',
+      ),
+    );
+  }
+
   AnalysisOptions _getOptionsObject(String filePath) =>
       AnalysisOptionsImpl.fromYaml(
         optionsMap: provider.getOptions(getFolder(filePath)),
+        file: getFile(filePath),
       );
 }