| // Copyright (c) 2025, 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/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'; |
| |
| import '../utils/test_code_extensions.dart'; |
| import 'server_abstract.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(InlineValueTest); |
| }); |
| } |
| |
| @reflectiveTest |
| 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_block_ifStatement_inside() async { |
| code = TestCode.parse(r''' |
| void f(int a, int b, int c) { |
| if (a == 1) { |
| /*[0*/a/*0]*/; |
| /*[1*/b/*1]*/; |
| /*[2*/c/*2]*/; |
| ^ |
| } |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_block_ifStatement_notInside() async { |
| code = TestCode.parse(r''' |
| void f(int a, int /*[0*/b/*0]*/, int c) { |
| if (/*[1*/a/*1]*/ == 1) { |
| // Code inside blocks is excluded |
| a; |
| b; |
| c; |
| } |
| ^/*[2*/c/*2]*/; |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_block_switchStatement() async { |
| code = TestCode.parse(r''' |
| void f(int a, int /*[0*/b/*0]*/, int c) { |
| switch (/*[1*/a/*1]*/) { |
| case 0: |
| // Ignored because not in this case |
| a; |
| b; |
| c; |
| case 1: |
| ^/*[2*/c/*2]*/; |
| } |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_iterables() async { |
| experimentalInlineValuesProperties = true; |
| |
| // There are no marked ranges, because none of these should produce values. |
| code = TestCode.parse(r''' |
| import 'dart:async'; |
| |
| void f( |
| Iterable<int> p1, |
| Future<int> p2, |
| FutureOr<int> p3, |
| Stream<int> p4, |
| ) { |
| ^ |
| } |
| '''); |
| |
| await verify_values(code); |
| } |
| |
| Future<void> test_parameter_declaration() async { |
| code = TestCode.parse(r''' |
| void f(int /*[0*/aaa/*0]*/, int /*[1*/bbb/*1]*/) { |
| ^ |
| aaa + bbb; |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| /// Lists are included, iterables are not. |
| Future<void> test_parameter_iterables() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| void f(List list1, List<int> /*[0*/list2/*0]*/, Iterable iterable1, Iterable iterable2) { |
| print(/*[1*/list1/*1]*/); |
| print(iterable1); |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_parameter_nestedInFunctionType() async { |
| // A parameter nested inside a function type should not produce a value. |
| code = TestCode.parse(r''' |
| void f( |
| int /*[0*/value/*0]*/, { |
| required void Function(int value) /*[1*/func/*1]*/, |
| }) { |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_parameter_read() async { |
| code = TestCode.parse(r''' |
| void f(int aaa, int bbb) { |
| ^/*[0*/aaa/*0]*/ + /*[1*/bbb/*1]*/; |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_parameter_write() async { |
| code = TestCode.parse(r''' |
| void f(int aaa, int bbb, int ccc) { |
| /*[0*/aaa/*0]*/++; |
| /*[1*/bbb/*1]*/ = 1; |
| /*[2*/ccc/*2]*/ += 1; |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_parameters_scope() async { |
| // We should only get the parameters declared in the current function. |
| code = TestCode.parse(r''' |
| void f(int aaa, int bbb, int ccc) { |
| void b(int /*[0*/aaa/*0]*/, int /*[1*/bbb/*1]*/) { |
| |
| var _ = (aaa, bbb, ccc) => null; |
| var _ = () { |
| var aaa = 1, bbb = 1, ccc = 1; |
| }; |
| |
| ^ |
| } |
| } |
| '''); |
| |
| 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_getter_enum_value_excluded() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| enum MyEnum { |
| one, |
| } |
| |
| void f(MyEnum x) { |
| print(/*[0*/x/*0]*/ == MyEnum.one); // MyEnum.one excluded |
| print(/*[1*/MyEnum.one.index/*1]*/); |
| ^ |
| } |
| '''); |
| |
| await verify_values( |
| code, |
| ofTypes: { |
| 0: InlineValueVariableLookup, |
| 1: InlineValueEvaluatableExpression, |
| }, |
| ); |
| } |
| |
| Future<void> test_property_getter_enum_values_excluded() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| enum MyEnum { |
| one, |
| } |
| |
| void f() { |
| print(MyEnum.values); // MyEnum.values excluded |
| print(/*[0*/MyEnum.values.length/*0]*/); |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueEvaluatableExpression); |
| } |
| |
| /// Lists are included, iterables are not. |
| Future<void> test_property_iterables() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| void f(List<int> /*[0*/list/*0]*/, Iterable<int> iterable) { |
| print(/*[1*/list.length/*1]*/); |
| print(iterable.length); |
| ^ |
| } |
| '''); |
| |
| await verify_values( |
| code, |
| ofTypes: { |
| 0: InlineValueVariableLookup, |
| 1: 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); |
| } |
| |
| /// Unlike variables, which we include for the line of the execution pointer |
| /// (to aid with reviewing conditional statements), getters are only evaluated |
| /// if they are before the execution pointer to reduce the chance of |
| /// triggering side-effects before the code would have. |
| Future<void> test_property_range_onlyBeforePointer() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| void f(String /*[0*/s/*0]*/) { |
| ^if (s.isNotEmpty) { |
| } |
| } |
| '''); |
| |
| 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 { |
| code = TestCode.parse(r''' |
| void f() { |
| int /*[0*/aaa/*0]*/ = 1; |
| int /*[1*/bbb/*1]*/ = 1, /*[2*/ccc/*2]*/ = 1; |
| ^ |
| aaa + bbb; |
| ccc; |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_variable_forIn() async { |
| code = TestCode.parse(r''' |
| void f(List<int> ints) { |
| for (var /*[0*/i/*0]*/ in /*[1*/ints/*1]*/) { |
| ^ |
| } |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_variable_forIn_destructure() async { |
| code = TestCode.parse(r''' |
| void f(List<(int, int)> records) { |
| for (var (/*[0*/x/*0]*/, /*[1*/y/*1]*/) in /*[2*/records/*2]*/) { |
| ^ |
| } |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| /// Lists are included, iterables are not. |
| Future<void> test_variable_iterables() async { |
| experimentalInlineValuesProperties = true; |
| |
| code = TestCode.parse(r''' |
| void f() { |
| var list = [1,]; |
| var iterable = list as Iterable<int>; |
| |
| print(/*[0*/list/*0]*/); |
| print(iterable); |
| ^ |
| } |
| '''); |
| |
| 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 { |
| code = TestCode.parse(r''' |
| void f() { |
| int aaa = 1; |
| int bbb = 1, ccc = 1; |
| ^/*[0*/aaa/*0]*/ + /*[1*/bbb/*1]*/ + /*[2*/ccc/*2]*/; |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_variable_write() async { |
| code = TestCode.parse(r''' |
| void f() { |
| int aaa = 0, bbb = 0, ccc = 0; |
| /*[0*/aaa/*0]*/ = 1; |
| /*[1*/bbb/*1]*/++; |
| /*[2*/ccc/*2]*/ += 1; |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| Future<void> test_variables_scope() async { |
| // We should not get the top-levels or the nested functions. |
| code = TestCode.parse(r''' |
| var aaa = 1; |
| var bbb = 1; |
| |
| void f() { |
| int /*[0*/aaa/*0]*/ = 0, /*[1*/ccc/*1]*/ = 0; |
| |
| var _ = (aaa, bbb, ccc) => null; |
| var _ = () { |
| var aaa = 1, bbb = 1, ccc = 1; |
| }; |
| |
| ^ |
| } |
| '''); |
| |
| await verify_values(code, ofType: InlineValueVariableLookup); |
| } |
| |
| /// 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; |
| |
| var actualValues = await getInlineValues( |
| mainFileUri, |
| visibleRange: rangeOfWholeContent(code.code), |
| stoppedAt: code.position.position, |
| ); |
| |
| 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)); |
| } |
| } |