[analysis_server] Support properties in line values behind a flag

+ fix them not showing up inside methods

Change-Id: I885afe68d56ab60c20b816373fbd071592cfd463
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412644
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/client_configuration.dart b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
index 3cf71d2..99004a2 100644
--- a/pkg/analysis_server/lib/src/lsp/client_configuration.dart
+++ b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
@@ -191,6 +191,10 @@
   bool get completeFunctionCalls =>
       _settings['completeFunctionCalls'] as bool? ?? false;
 
+  /// A flag for including property access in Inline Values.
+  bool get experimentalInlineValuesProperties =>
+      _settings['experimentalInlineValuesProperties'] as bool? ?? false;
+
   /// A flag for enabling interactive refactors flagged as experimental.
   ///
   /// This flag is likely to be used by both analysis server developers (working
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_inline_value.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_inline_value.dart
index 5331f51..4900b76 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_inline_value.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_inline_value.dart
@@ -4,11 +4,13 @@
 
 import 'package:analysis_server/lsp_protocol/protocol.dart' hide MessageType;
 import 'package:analysis_server/lsp_protocol/protocol.dart';
+import 'package:analysis_server/src/lsp/client_configuration.dart';
 import 'package:analysis_server/src/lsp/error_or.dart';
 import 'package:analysis_server/src/lsp/extensions/positions.dart';
 import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/mapping.dart';
 import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
+import 'package:analysis_server/src/services/correction/dart/convert_null_check_to_null_aware_element_or_entry.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element2.dart';
@@ -66,13 +68,19 @@
         // Find the function that is executing. We will only show values for
         // this single function expression.
         var node = await server.getNodeAtOffset(filePath, stoppedOffset);
-        var function = node?.thisOrAncestorOfType<FunctionExpression>();
+        var function = node?.thisOrAncestorMatching(
+          (node) => node is FunctionExpression || node is MethodDeclaration,
+        );
         if (function == null) {
           return success(null);
         }
 
         var collector = _InlineValueCollector(lineInfo, applicableRange);
-        var visitor = _InlineValueVisitor(collector, function);
+        var visitor = _InlineValueVisitor(
+          server.lspClientConfiguration,
+          collector,
+          function,
+        );
         function.accept(visitor);
 
         return success(collector.values.values.toList());
@@ -117,19 +125,64 @@
 
   _InlineValueCollector(this.lineInfo, this.applicableRange);
 
-  /// Records a variable inline value for [element] with [offset]/[length].
+  /// Records an expression inline value for [element] with [offset]/[length].
   ///
-  /// Variable inline values are sent to the client without expressions/names
-  /// because the client can infer the name from the range and look it up from
-  /// the debuggers Scopes/Variables.
-  void recordVariableLookup(Element2? element, int offset, int length) {
-    if (element == null || element.isWildcardVariable) return;
-
+  /// Expression values are sent to the client without expressions because the
+  /// client can use the range from the source to get the expression.
+  void recordExpression(Element2? element, int offset, int length) {
     assert(offset >= 0);
     assert(length > 0);
+    if (element == null) return;
 
     var range = toRange(lineInfo, offset, length);
 
+    var value = InlineValue.t1(
+      InlineValueEvaluatableExpression(
+        range: range,
+        // We don't provide expression, because it always matches the source
+        // code and can be inferred.
+      ),
+    );
+    _record(value, element);
+  }
+
+  /// Records a variable inline value for [element] with [offset]/[length].
+  ///
+  /// Variable inline values are sent to the client without names because the
+  /// client can infer the name from the range and look it up from the debuggers
+  /// Scopes/Variables.
+  void recordVariableLookup(Element2? element, int offset, int length) {
+    assert(offset >= 0);
+    assert(length > 0);
+    if (element == null || element.isWildcardVariable) return;
+
+    var range = toRange(lineInfo, offset, length);
+
+    var value = InlineValue.t3(
+      InlineValueVariableLookup(
+        caseSensitiveLookup: true,
+        range: range,
+        // We don't provide name, because it always matches the source code
+        // for a variable and can be inferred.
+      ),
+    );
+    _record(value, element);
+  }
+
+  /// Extracts the range from an [InlineValue].
+  Range _getRange(InlineValue value) {
+    return value.map(
+      (expression) => expression.range,
+      (text) => text.range,
+      (variable) => variable.range,
+    );
+  }
+
+  /// Records an inline value [value] for [element] if it is within range and is
+  /// the latest one in the source for that element.
+  void _record(InlineValue value, Element2 element) {
+    var range = _getRange(value);
+
     // Never record anything outside of the visible range.
     if (!range.intersects(applicableRange)) {
       return;
@@ -137,34 +190,28 @@
 
     // We only want to show each variable once, so keep only the one furthest
     // into the source (closest to the execution pointer).
-    var existingPosition = values[element]?.map(
-      (expression) => expression.range.start,
-      (text) => text.range.start,
-      (variable) => variable.range.start,
-    );
-    if (existingPosition != null &&
-        existingPosition.isAfterOrEqual(range.start)) {
-      return;
+    if (values[element] case var existingValue?) {
+      var existingPosition = _getRange(existingValue).start;
+      if (existingPosition.isAfterOrEqual(range.start)) {
+        return;
+      }
     }
 
-    values[element] = InlineValue.t3(
-      InlineValueVariableLookup(
-        caseSensitiveLookup: true,
-        range: range,
-        // We don't provide name, because it always matches the source code
-        // for a variable and can be inferred.
-      ),
-    );
+    values[element] = value;
   }
 }
 
 /// Visits a function expression and reports nodes that should have inline
 /// values to [collector].
 class _InlineValueVisitor extends GeneralizingAstVisitor<void> {
+  final LspClientConfiguration clientConfiguration;
   final _InlineValueCollector collector;
-  final FunctionExpression rootFunction;
+  final AstNode rootNode;
 
-  _InlineValueVisitor(this.collector, this.rootFunction);
+  _InlineValueVisitor(this.clientConfiguration, this.collector, this.rootNode);
+
+  bool get experimentalInlineValuesProperties =>
+      clientConfiguration.global.experimentalInlineValuesProperties;
 
   @override
   void visitFormalParameter(FormalParameter node) {
@@ -182,7 +229,7 @@
   @override
   void visitFunctionExpression(FunctionExpression node) {
     // Don't descend into nested functions.
-    if (node != rootFunction) {
+    if (node != rootNode) {
       return;
     }
 
@@ -190,12 +237,53 @@
   }
 
   @override
-  void visitSimpleIdentifier(SimpleIdentifier node) {
-    switch (node.element) {
-      case LocalVariableElement2(name3: _?):
-      case FormalParameterElement():
-        collector.recordVariableLookup(node.element, node.offset, node.length);
+  void visitPrefixedIdentifier(PrefixedIdentifier node) {
+    if (experimentalInlineValuesProperties) {
+      var parent = node.parent;
+
+      // Never produce values for the left side of a property access.
+      var isTarget = parent is PropertyAccess && node == parent.realTarget;
+      if (!isTarget) {
+        collector.recordExpression(node.element, node.offset, node.length);
+      }
     }
+
+    super.visitPrefixedIdentifier(node);
+  }
+
+  @override
+  void visitPropertyAccess(PropertyAccess node) {
+    if (experimentalInlineValuesProperties && node.target is Identifier) {
+      collector.recordExpression(
+        node.canonicalElement,
+        node.offset,
+        node.length,
+      );
+    }
+
+    super.visitPropertyAccess(node);
+  }
+
+  @override
+  void visitSimpleIdentifier(SimpleIdentifier node) {
+    var parent = node.parent;
+
+    // Never produce values for the left side of a prefixed identifier.
+    // Or parts of an invocation.
+    var isTarget = parent is PrefixedIdentifier && node == parent.prefix;
+    var isInvocation = parent is InvocationExpression;
+    if (!isTarget && !isInvocation) {
+      switch (node.element) {
+        case LocalVariableElement2(name3: _?):
+        case FormalParameterElement():
+          collector.recordVariableLookup(
+            node.element,
+            node.offset,
+            node.length,
+          );
+      }
+    }
+
     super.visitSimpleIdentifier(node);
   }
 
diff --git a/pkg/analysis_server/test/lsp/inline_value_test.dart b/pkg/analysis_server/test/lsp/inline_value_test.dart
index 6d386fb..d61f50f 100644
--- a/pkg/analysis_server/test/lsp/inline_value_test.dart
+++ b/pkg/analysis_server/test/lsp/inline_value_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:analysis_server/lsp_protocol/protocol.dart';
 import 'package:analyzer/src/test_utilities/test_code_format.dart';
+import 'package:collection/collection.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
@@ -20,6 +21,10 @@
 class InlineValueTest extends AbstractLspAnalysisServerTest {
   late TestCode code;
 
+  /// Whether to enable the inlineValuesProperties experiment flag in the
+  /// client configuration passed during initialization.
+  bool experimentalInlineValuesProperties = false;
+
   Future<void> test_parameter_declaration() async {
     code = TestCode.parse(r'''
 void f(int /*[0*/aaa/*0]*/, int /*[1*/bbb/*1]*/) {
@@ -28,7 +33,7 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_parameter_read() async {
@@ -38,7 +43,7 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_parameter_write() async {
@@ -51,7 +56,7 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_parameters_scope() async {
@@ -70,7 +75,156 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_property_experimentDisabled() async {
+    code = TestCode.parse(r'''
+void f(String /*[0*/s/*0]*/) {
+  print(s.length); // No inline value, experiment not enabled
+  ^
+}
+''');
+
+    await verify_values(
+      code,
+      // The parameter is a variable even though this test is about properties.
+      ofType: InlineValueVariableLookup,
+    );
+  }
+
+  Future<void> test_property_getter() async {
+    experimentalInlineValuesProperties = true;
+
+    code = TestCode.parse(r'''
+void f(String /*[0*/s/*0]*/) {
+  print(/*[1*/s.length/*1]*/);
+  print(/*[2*/s.length.isEven/*2]*/);
+  ^
+}
+''');
+
+    await verify_values(
+      code,
+      ofTypes: {
+        0: InlineValueVariableLookup,
+        1: InlineValueEvaluatableExpression,
+        2: InlineValueEvaluatableExpression,
+      },
+    );
+  }
+
+  Future<void> test_property_method() async {
+    experimentalInlineValuesProperties = true;
+
+    code = TestCode.parse(r'''
+class A {
+  void x() {}
+}
+void f(A /*[0*/a/*0]*/) {
+  a.x(); // No value for methods.
+  ^
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_property_method_targets() async {
+    experimentalInlineValuesProperties = true;
+
+    code = TestCode.parse(r'''
+class A {
+  String x(int a) => a.toString();
+}
+void f(A /*[0*/a/*0]*/, int b) {
+  // No value for length because the expression contains a method call.
+  a.x(b).length;
+
+  // No value for either length of isEven because the expression contains a
+  // method call.
+  a.x(/*[1*/b/*1]*/).length.isEven;
+  ^
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_property_setter() async {
+    experimentalInlineValuesProperties = true;
+
+    code = TestCode.parse(r'''
+class A {
+  int? x;
+}
+void f(A /*[0*/a/*0]*/) {
+  a.x = 1; // No value for setters.
+  ^
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_scope_method_inNestedFunction() async {
+    code = TestCode.parse(r'''
+class A {
+  void method() {
+    void inner() {
+      var [!valueVar!] = 1;
+      ^
+    }
+    var noValueVar = 1;
+  }
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_scope_method_notInNestedFunction() async {
+    code = TestCode.parse(r'''
+class A {
+  void method() {
+    void inner() {
+      var noValueVar = 1;
+    }
+    var [!valueVar!] = 1;
+    ^
+  }
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_scope_topLevelFunction_inNestedFunction() async {
+    code = TestCode.parse(r'''
+void top() {
+  void inner() {
+    var [!valueVar!] = 1;
+    ^
+  }
+  var noValueVar = 1;
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_scope_topLevelFunction_notInNestedFunction() async {
+    code = TestCode.parse(r'''
+void top() {
+  void inner() {
+    var noValueVar = 1;
+  }
+  var [!valueVar!] = 1;
+  ^
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_variable_declaration() async {
@@ -84,7 +238,18 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
+  }
+
+  Future<void> test_variable_propertyAccess() async {
+    code = TestCode.parse(r'''
+void f(int /*[0*/aaa/*0]*/) {
+  aaa.isEven; // No inline value for var because it's in a property access.
+  ^
+}
+''');
+
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_variable_read() async {
@@ -96,7 +261,7 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_variable_write() async {
@@ -110,7 +275,7 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
   Future<void> test_variables_scope() async {
@@ -131,13 +296,23 @@
 }
 ''');
 
-    await verify_variables(code);
+    await verify_values(code, ofType: InlineValueVariableLookup);
   }
 
-  /// Verifies [code] produces the [expected] variables (and only those
-  /// variables).
-  Future<void> verify_variables(TestCode code) async {
-    await initialize();
+  /// Verifies [code] produces values at the marked ranges.
+  ///
+  /// The [ofTypes] contains the kind of value to be expected for the range with
+  /// the same index. If a range is not included in [ofTypes] then [ofType] is
+  /// used instead.
+  Future<void> verify_values(
+    TestCode code, {
+    Type? ofType,
+    Map<int, Type>? ofTypes,
+  }) async {
+    await provideConfig(initialize, {
+      if (experimentalInlineValuesProperties)
+        'experimentalInlineValuesProperties': true,
+    });
     await openFile(mainFileUri, code.code);
     await initialAnalysis;
 
@@ -147,11 +322,17 @@
       stoppedAt: code.position.position,
     );
 
-    var expectedValues = code.ranges.ranges.map(
-      (range) => InlineValue.t3(
-        InlineValueVariableLookup(caseSensitiveLookup: true, range: range),
-      ),
-    );
+    var expectedValues = code.ranges.ranges.mapIndexed((index, range) {
+      return switch (ofTypes?[index] ?? ofType) {
+        const (InlineValueVariableLookup) => InlineValue.t3(
+          InlineValueVariableLookup(caseSensitiveLookup: true, range: range),
+        ),
+        const (InlineValueEvaluatableExpression) => InlineValue.t1(
+          InlineValueEvaluatableExpression(range: range),
+        ),
+        _ => throw 'No type provided for range $index',
+      };
+    });
 
     expect(actualValues, unorderedEquals(expectedValues));
   }