Add support for providing fixes in analysis options files

Change-Id: I6c9a0a0624201538a90608c2eff9cff7b6cdb4b8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/98870
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/edit/edit_domain.dart b/pkg/analysis_server/lib/src/edit/edit_domain.dart
index 6b7a5a9..12a1d18 100644
--- a/pkg/analysis_server/lib/src/edit/edit_domain.dart
+++ b/pkg/analysis_server/lib/src/edit/edit_domain.dart
@@ -22,6 +22,7 @@
 import 'package:analysis_server/src/services/correction/assist_internal.dart';
 import 'package:analysis_server/src/services/correction/change_workspace.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/correction/fix/analysis_options/fix_generator.dart';
 import 'package:analysis_server/src/services/correction/fix_internal.dart';
 import 'package:analysis_server/src/services/correction/organize_directives.dart';
 import 'package:analysis_server/src/services/correction/sort_members.dart';
@@ -33,16 +34,23 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/error/error.dart' as engine;
+import 'package:analyzer/file_system/file_system.dart';
+// ignore: deprecated_member_use
+import 'package:analyzer/source/analysis_options_provider.dart';
+import 'package:analyzer/source/line_info.dart';
 import 'package:analyzer/src/dart/ast/utilities.dart';
 import 'package:analyzer/src/dart/scanner/scanner.dart' as engine;
 import 'package:analyzer/src/error/codes.dart' as engine;
 import 'package:analyzer/src/generated/engine.dart' as engine;
+import 'package:analyzer/src/generated/engine.dart';
 import 'package:analyzer/src/generated/parser.dart' as engine;
 import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/task/options.dart';
 import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
 import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin;
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 import 'package:dart_style/dart_style.dart';
+import 'package:yaml/yaml.dart';
 
 int test_resetCount = 0;
 
@@ -231,9 +239,7 @@
       new EditGetDartfixInfoResult(allFixes.map((i) => i.asDartFix()).toList())
           .toResponse(request.id);
 
-  Future getFixes(Request request) async {
-    // TODO(brianwilkerson) Determine whether this await is necessary.
-    await null;
+  Future<void> getFixes(Request request) async {
     EditGetFixesParams params = new EditGetFixesParams.fromRequest(request);
     String file = params.file;
     int offset = params.offset;
@@ -241,7 +247,6 @@
     if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
       return;
     }
-
     //
     // Allow plugins to start computing fixes.
     //
@@ -569,12 +574,49 @@
   }
 
   /**
-   * Compute and return the fixes associated with server-generated errors.
+   * Compute and return the fixes associated with server-generated errors in
+   * analysis options files.
    */
-  Future<List<AnalysisErrorFixes>> _computeServerErrorFixes(
+  Future<List<AnalysisErrorFixes>> _computeAnalysisOptionsFixes(
       String file, int offset) async {
-    // TODO(brianwilkerson) Determine whether this await is necessary.
-    await null;
+    List<AnalysisErrorFixes> errorFixesList = <AnalysisErrorFixes>[];
+    File optionsFile = server.resourceProvider.getFile(file);
+    String content = _safelyRead(optionsFile);
+    if (content == null) {
+      return errorFixesList;
+    }
+    LineInfo lineInfo = new LineInfo.fromContent(content);
+    SourceFactory sourceFactory = server.getAnalysisDriver(file).sourceFactory;
+    List<engine.AnalysisError> errors = analyzeAnalysisOptions(
+        optionsFile.createSource(), content, sourceFactory);
+    YamlMap options = _getOptions(sourceFactory, content);
+    if (options == null) {
+      return errorFixesList;
+    }
+    for (engine.AnalysisError error in errors) {
+      AnalysisOptionsFixGenerator generator =
+          new AnalysisOptionsFixGenerator(error, content, options);
+      List<Fix> fixes = await generator.computeFixes();
+      if (fixes.isNotEmpty) {
+        fixes.sort(Fix.SORT_BY_RELEVANCE);
+        AnalysisError serverError =
+            newAnalysisError_fromEngine(lineInfo, error);
+        AnalysisErrorFixes errorFixes = new AnalysisErrorFixes(serverError);
+        errorFixesList.add(errorFixes);
+        fixes.forEach((fix) {
+          errorFixes.fixes.add(fix.change);
+        });
+      }
+    }
+    return errorFixesList;
+  }
+
+  /**
+   * Compute and return the fixes associated with server-generated errors in
+   * Dart files.
+   */
+  Future<List<AnalysisErrorFixes>> _computeDartFixes(
+      String file, int offset) async {
     List<AnalysisErrorFixes> errorFixesList = <AnalysisErrorFixes>[];
     var result = await server.getResolvedUnit(file);
     if (result != null) {
@@ -603,6 +645,20 @@
     return errorFixesList;
   }
 
+  /**
+   * Compute and return the fixes associated with server-generated errors.
+   */
+  Future<List<AnalysisErrorFixes>> _computeServerErrorFixes(
+      String file, int offset) async {
+    if (AnalysisEngine.isDartFileName(file)) {
+      return _computeDartFixes(file, offset);
+    } else if (AnalysisEngine.isAnalysisOptionsFileName(
+        file, server.resourceProvider.pathContext)) {
+      return _computeAnalysisOptionsFixes(file, offset);
+    }
+    return <AnalysisErrorFixes>[];
+  }
+
   Response _getAvailableRefactorings(Request request) {
     _getAvailableRefactoringsImpl(request);
     return Response.DELAYED_RESPONSE;
@@ -676,6 +732,16 @@
     server.sendResponse(result.toResponse(request.id));
   }
 
+  YamlMap _getOptions(SourceFactory sourceFactory, String content) {
+    AnalysisOptionsProvider optionsProvider =
+        new AnalysisOptionsProvider(sourceFactory);
+    try {
+      return optionsProvider.getOptionsFromString(content);
+    } on OptionsFormatException {
+      return null;
+    }
+  }
+
   Response _getRefactoring(Request request) {
     if (refactoringManager.hasPendingRequest) {
       refactoringManager.cancel();
@@ -692,6 +758,16 @@
     refactoringManager = new _RefactoringManager(server, refactoringWorkspace);
   }
 
+  /// Return the contents of the [file], or `null` if the file does not exist or
+  /// cannot be read.
+  String _safelyRead(File file) {
+    try {
+      return file.readAsStringSync();
+    } on FileSystemException {
+      return null;
+    }
+  }
+
   static int _getNumberOfScanParseErrors(List<engine.AnalysisError> errors) {
     int numScanParseErrors = 0;
     for (engine.AnalysisError error in errors) {
diff --git a/pkg/analysis_server/lib/src/services/correction/fix.dart b/pkg/analysis_server/lib/src/services/correction/fix.dart
index dbbc858..3c54bcb 100644
--- a/pkg/analysis_server/lib/src/services/correction/fix.dart
+++ b/pkg/analysis_server/lib/src/services/correction/fix.dart
@@ -11,9 +11,7 @@
 import 'package:analyzer_plugin/utilities/change_builder/change_workspace.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 
-/**
- * Return true if this [errorCode] is likely to have a fix associated with it.
- */
+/// Return true if this [errorCode] is likely to have a fix associated with it.
 bool hasFix(ErrorCode errorCode) =>
     errorCode == StaticWarningCode.UNDEFINED_CLASS_BOOLEAN ||
     errorCode == StaticWarningCode.CONCRETE_CLASS_WITH_ABSTRACT_MEMBER ||
@@ -103,9 +101,14 @@
             errorCode.name == LintNames.unnecessary_this ||
             errorCode.name == LintNames.use_rethrow_when_possible));
 
-/**
- * The implementation of [DartFixContext].
- */
+/// An enumeration of quick fix kinds for the errors found in an analysis
+/// options file.
+class AnalysisOptionsFixKind {
+  static const REMOVE_SETTING =
+      const FixKind('REMOVE_SETTING', 50, "Remove '{0}'");
+}
+
+/// The implementation of [DartFixContext].
 class DartFixContextImpl implements DartFixContext {
   @override
   final ChangeWorkspace workspace;
@@ -119,9 +122,7 @@
   DartFixContextImpl(this.workspace, this.resolveResult, this.error);
 }
 
-/**
- * An enumeration of possible quick fix kinds.
- */
+/// An enumeration of quick fix kinds found in a Dart file.
 class DartFixKind {
   static const ADD_ASYNC =
       const FixKind('ADD_ASYNC', 50, "Add 'async' modifier");
diff --git a/pkg/analysis_server/lib/src/services/correction/fix/analysis_options/fix_generator.dart b/pkg/analysis_server/lib/src/services/correction/fix/analysis_options/fix_generator.dart
new file mode 100644
index 0000000..f558372
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/correction/fix/analysis_options/fix_generator.dart
@@ -0,0 +1,238 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:analysis_server/plugin/edit/fix/fix_core.dart';
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/correction/strings.dart';
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer/source/source_range.dart';
+import 'package:analyzer/src/analysis_options/error/option_codes.dart';
+import 'package:analyzer/src/generated/java_core.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart'
+    show SourceChange;
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:meta/meta.dart';
+import 'package:source_span/src/span.dart';
+import 'package:yaml/yaml.dart';
+
+/// The generator used to generate fixes in analysis options files.
+class AnalysisOptionsFixGenerator {
+  final AnalysisError error;
+
+  final int errorOffset;
+
+  final int errorLength;
+
+  final String content;
+
+  final YamlMap options;
+
+  final LineInfo lineInfo;
+
+  final List<Fix> fixes = <Fix>[];
+
+  List<YamlNode> coveringNodePath;
+
+  AnalysisOptionsFixGenerator(this.error, this.content, this.options)
+      : errorOffset = error.offset,
+        errorLength = error.length,
+        lineInfo = new LineInfo.fromContent(content);
+
+  /// Return the absolute, normalized path to the file in which the error was
+  /// reported.
+  String get file => error.source.fullName;
+
+  /// Return the list of fixes that apply to the error being fixed.
+  Future<List<Fix>> computeFixes() async {
+    YamlNodeLocator locator = new YamlNodeLocator(
+        start: errorOffset, end: errorOffset + errorLength - 1);
+    coveringNodePath = locator.searchWithin(options);
+    if (coveringNodePath.isEmpty) {
+      return fixes;
+    }
+
+    ErrorCode errorCode = error.errorCode;
+//    if (errorCode == AnalysisOptionsErrorCode.INCLUDED_FILE_PARSE_ERROR) {
+//    } else if (errorCode == AnalysisOptionsErrorCode.PARSE_ERROR) {
+//    } else if (errorCode ==
+//        AnalysisOptionsHintCode.DEPRECATED_ANALYSIS_OPTIONS_FILE_NAME) {
+//    } else if (errorCode ==
+//        AnalysisOptionsHintCode.PREVIEW_DART_2_SETTING_DEPRECATED) {
+//    } else if (errorCode ==
+//        AnalysisOptionsHintCode.STRONG_MODE_SETTING_DEPRECATED) {
+//    } else
+    if (errorCode == AnalysisOptionsHintCode.SUPER_MIXINS_SETTING_DEPRECATED) {
+      await _addFix_removeSetting();
+//    } else if (errorCode ==
+//        AnalysisOptionsWarningCode.ANALYSIS_OPTION_DEPRECATED) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.INCLUDED_FILE_WARNING) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.INCLUDE_FILE_NOT_FOUND) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.INVALID_OPTION) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.INVALID_SECTION_FORMAT) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.SPEC_MODE_REMOVED) {
+//    } else if (errorCode ==
+//        AnalysisOptionsWarningCode.UNRECOGNIZED_ERROR_CODE) {
+    } else if (errorCode ==
+        AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITHOUT_VALUES) {
+      await _addFix_removeSetting();
+//    } else if (errorCode ==
+//        AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUE) {
+//    } else if (errorCode ==
+//        AnalysisOptionsWarningCode.UNSUPPORTED_OPTION_WITH_LEGAL_VALUES) {
+//    } else if (errorCode == AnalysisOptionsWarningCode.UNSUPPORTED_VALUE) {
+    }
+    return fixes;
+  }
+
+  void _addFix_removeSetting() async {
+    if (coveringNodePath[0] is YamlScalar) {
+      SourceRange deletionRange;
+      int index = 1;
+      while (index < coveringNodePath.length) {
+        YamlNode parent = coveringNodePath[index];
+        if (parent is YamlList) {
+          if (parent.nodes.length > 1) {
+            YamlNode nodeToDelete = coveringNodePath[index - 1];
+            deletionRange = _lines(
+                nodeToDelete.span.start.offset, nodeToDelete.span.end.offset);
+            break;
+          }
+        } else if (parent is YamlMap) {
+          Map<dynamic, YamlNode> nodes = parent.nodes;
+          if (nodes.length > 1) {
+            YamlNode key;
+            YamlNode value;
+            YamlNode child = coveringNodePath[index - 1];
+            if (nodes.containsKey(child)) {
+              key = child;
+              value = nodes[child];
+            } else if (nodes.containsValue(child)) {
+              for (var entry in nodes.entries) {
+                if (child == entry.value) {
+                  key = entry.key;
+                  value = child;
+                  break;
+                }
+              }
+            }
+            if (key == null || value == null) {
+              throw StateError(
+                  'Child is neither a key nor a value in the parent');
+            }
+            deletionRange = _lines(key.span.start.offset,
+                _firstNonWhitespaceBefore(value.span.end.offset));
+            break;
+          }
+        } else if (parent is YamlDocument) {
+          break;
+        }
+        index++;
+      }
+      YamlNode nodeToDelete = coveringNodePath[index - 1];
+      deletionRange ??=
+          _lines(nodeToDelete.span.start.offset, nodeToDelete.span.end.offset);
+      ChangeBuilder builder = new ChangeBuilder();
+      await builder.addFileEdit(file, (builder) {
+        builder.addDeletion(deletionRange);
+      });
+      _addFixFromBuilder(builder, AnalysisOptionsFixKind.REMOVE_SETTING,
+          args: [coveringNodePath[0].toString()]);
+    }
+  }
+
+  /// Add a fix whose edits were built by the [builder] that has the given
+  /// [kind]. If [args] are provided, they will be used to fill in the message
+  /// for the fix.
+  void _addFixFromBuilder(ChangeBuilder builder, FixKind kind,
+      {List args: null}) {
+    SourceChange change = builder.sourceChange;
+    if (change.edits.isEmpty) {
+      return;
+    }
+    change.message = formatList(kind.message, args);
+    fixes.add(new Fix(kind, change));
+  }
+
+  int _firstNonWhitespaceBefore(int offset) {
+    while (offset > 0 && isWhitespace(content.codeUnitAt(offset - 1))) {
+      offset--;
+    }
+    return offset;
+  }
+
+  SourceRange _lines(int start, int end) {
+    CharacterLocation startLocation = lineInfo.getLocation(start);
+    int startOffset = lineInfo.getOffsetOfLine(startLocation.lineNumber - 1);
+    CharacterLocation endLocation = lineInfo.getLocation(end);
+    int endOffset = lineInfo.getOffsetOfLine(
+        math.min(endLocation.lineNumber, lineInfo.lineCount - 1));
+    return new SourceRange(startOffset, endOffset - startOffset);
+  }
+}
+
+/// An object used to locate the [YamlNode] associated with a source range.
+/// More specifically, it will return the deepest [YamlNode] which completely
+/// encompasses the specified range.
+class YamlNodeLocator {
+  /// The inclusive start offset of the range used to identify the node.
+  int _startOffset = 0;
+
+  /// The inclusive end offset of the range used to identify the node.
+  int _endOffset = 0;
+
+  /// Initialize a newly created locator to locate the deepest [YamlNode] for
+  /// which `node.offset <= [start]` and `[end] < node.end`.
+  ///
+  /// If the [end] offset is not provided, then it is considered the same as the
+  /// [start] offset.
+  YamlNodeLocator({@required int start, int end})
+      : this._startOffset = start,
+        this._endOffset = end ?? start;
+
+  /// Search within the given Yaml [node] and return the path to the most deeply
+  /// nested node that includes the whole target range, or an empty list if no
+  /// node was found. The path is represented by all of the elements from the
+  /// starting [node] to the most deeply nested node, in reverse order.
+  List<YamlNode> searchWithin(YamlNode node) {
+    List<YamlNode> path = [];
+    _searchWithin(path, node);
+    return path;
+  }
+
+  void _searchWithin(List<YamlNode> path, YamlNode node) {
+    SourceSpan span = node.span;
+    if (span.start.offset > _endOffset || span.end.offset < _startOffset) {
+      return;
+    }
+    if (node is YamlList) {
+      for (YamlNode element in node.nodes) {
+        _searchWithin(path, element);
+        if (path.isNotEmpty) {
+          path.add(node);
+          return;
+        }
+      }
+    } else if (node is YamlMap) {
+      Map<dynamic, YamlNode> nodeMap = node.nodes;
+      for (YamlNode key in nodeMap.keys) {
+        _searchWithin(path, key);
+        if (path.isNotEmpty) {
+          path.add(node);
+          return;
+        }
+        _searchWithin(path, nodeMap[key]);
+        if (path.isNotEmpty) {
+          path.add(node);
+          return;
+        }
+      }
+    }
+    path.add(node);
+  }
+}
diff --git a/pkg/analysis_server/test/edit/test_all.dart b/pkg/analysis_server/test/edit/test_all.dart
index 79cc139..70251a6 100644
--- a/pkg/analysis_server/test/edit/test_all.dart
+++ b/pkg/analysis_server/test/edit/test_all.dart
@@ -4,24 +4,24 @@
 
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
-import 'assists_test.dart' as assists_test;
-import 'fixes_test.dart' as fixes_test;
-import 'format_test.dart' as format_test;
-import 'organize_directives_test.dart' as organize_directives_test;
-import 'postfix_completion_test.dart' as postfix_completion_test;
-import 'refactoring_test.dart' as refactoring_test;
-import 'sort_members_test.dart' as sort_members_test;
-import 'statement_completion_test.dart' as statement_completion_test;
+import 'assists_test.dart' as assists;
+import 'fixes_test.dart' as fixes;
+import 'format_test.dart' as format;
+import 'organize_directives_test.dart' as organize_directives;
+import 'postfix_completion_test.dart' as postfix_completion;
+import 'refactoring_test.dart' as refactoring;
+import 'sort_members_test.dart' as sort_members;
+import 'statement_completion_test.dart' as statement_completion;
 
 main() {
   defineReflectiveSuite(() {
-    assists_test.main();
-    fixes_test.main();
-    format_test.main();
-    organize_directives_test.main();
-    postfix_completion_test.main();
-    refactoring_test.main();
-    sort_members_test.main();
-    statement_completion_test.main();
+    assists.main();
+    fixes.main();
+    format.main();
+    organize_directives.main();
+    postfix_completion.main();
+    refactoring.main();
+    sort_members.main();
+    statement_completion.main();
   }, name: 'edit');
 }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/analysis_options/remove_setting_test.dart b/pkg/analysis_server/test/src/services/correction/fix/analysis_options/remove_setting_test.dart
new file mode 100644
index 0000000..5a19c8b
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/analysis_options/remove_setting_test.dart
@@ -0,0 +1,122 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/plugin/edit/fix/fix_core.dart';
+import 'package:analysis_server/src/protocol_server.dart' show SourceEdit;
+import 'package:analysis_server/src/services/correction/fix/analysis_options/fix_generator.dart';
+import 'package:analyzer/error/error.dart' as engine;
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
+import 'package:analyzer/src/generated/source.dart';
+import 'package:analyzer/src/task/options.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart'
+    show SourceFileEdit;
+import 'package:analyzer_plugin/protocol/protocol_common.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+import 'package:yaml/src/yaml_node.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(RemoveSettingTest);
+  });
+}
+
+class NonDartFixTest with ResourceProviderMixin {
+  Future<void> assertHasFix(
+      String initialContent, String location, String expectedContent) async {
+    File optionsFile = resourceProvider.getFile('/analysis_options.yaml');
+    SourceFactory sourceFactory = new SourceFactory([]);
+    List<engine.AnalysisError> errors = analyzeAnalysisOptions(
+        optionsFile.createSource(), initialContent, sourceFactory);
+    expect(errors, hasLength(1));
+    engine.AnalysisError error = errors[0];
+    YamlMap options = _getOptions(sourceFactory, initialContent);
+    AnalysisOptionsFixGenerator generator =
+        new AnalysisOptionsFixGenerator(error, initialContent, options);
+    List<Fix> fixes = await generator.computeFixes();
+    expect(fixes, hasLength(1));
+    List<SourceFileEdit> fileEdits = fixes[0].change.edits;
+    expect(fileEdits, hasLength(1));
+
+    String actualContent =
+        SourceEdit.applySequence(initialContent, fileEdits[0].edits);
+    expect(actualContent, expectedContent);
+  }
+
+  YamlMap _getOptions(SourceFactory sourceFactory, String content) {
+    AnalysisOptionsProvider optionsProvider =
+        new AnalysisOptionsProvider(sourceFactory);
+    try {
+      return optionsProvider.getOptionsFromString(content);
+    } on OptionsFormatException {
+      return null;
+    }
+  }
+}
+
+@reflectiveTest
+class RemoveSettingTest extends NonDartFixTest {
+  test_enableSuperMixins() async {
+    await assertHasFix(
+        '''
+analyzer:
+  enable-experiment:
+    - non-nullable
+  language:
+    enableSuperMixins: true
+''',
+        'enable',
+        '''
+analyzer:
+  enable-experiment:
+    - non-nullable
+''');
+  }
+
+  test_invalidExperiment_first() async {
+    await assertHasFix(
+        '''
+analyzer:
+  enable-experiment:
+    - not-an-experiment
+    - non-nullable
+''',
+        'not-',
+        '''
+analyzer:
+  enable-experiment:
+    - non-nullable
+''');
+  }
+
+  test_invalidExperiment_last() async {
+    await assertHasFix(
+        '''
+analyzer:
+  enable-experiment:
+    - non-nullable
+    - not-an-experiment
+''',
+        'not-',
+        '''
+analyzer:
+  enable-experiment:
+    - non-nullable
+''');
+  }
+
+  test_invalidExperiment_only() async {
+    await assertHasFix(
+        '''
+analyzer:
+  enable-experiment:
+    - not-an-experiment
+''',
+        'not-',
+        '''
+''');
+  }
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/analysis_options/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/analysis_options/test_all.dart
new file mode 100644
index 0000000..c202ee9
--- /dev/null
+++ b/pkg/analysis_server/test/src/services/correction/fix/analysis_options/test_all.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'remove_setting_test.dart' as remove_setting;
+
+main() {
+  defineReflectiveSuite(() {
+    remove_setting.main();
+  });
+}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
index 801f95b..de8cd2a 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/test_all.dart
@@ -23,6 +23,7 @@
 import 'add_static_test.dart' as add_static;
 import 'add_super_constructor_invocation_test.dart'
     as add_super_constructor_invocation;
+import 'analysis_options/test_all.dart' as analysis_options;
 import 'change_argument_name_test.dart' as change_argument_name;
 import 'change_to_nearest_precise_value_test.dart'
     as change_to_nearest_precise_value;
@@ -124,6 +125,7 @@
     add_required.main();
     add_static.main();
     add_super_constructor_invocation.main();
+    analysis_options.main();
     change_argument_name.main();
     change_to.main();
     change_to_nearest_precise_value.main();