blob: de451ff8c987af39c4b9d059a1a26618a65b1f71 [file] [log] [blame]
// 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' 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/element.dart' as analyzer;
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
import 'package:analyzer/utilities/extensions/ast.dart';
typedef StaticOptions =
Either3<bool, InlineValueOptions, InlineValueRegistrationOptions>;
class InlineValueHandler
extends
SharedMessageHandler<InlineValueParams, TextDocumentInlineValueResult> {
InlineValueHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_inlineValue;
@override
LspJsonHandler<InlineValueParams> get jsonHandler =>
InlineValueParams.jsonHandler;
@override
bool get requiresTrustedCaller => false;
@override
Future<ErrorOr<TextDocumentInlineValueResult>> handle(
InlineValueParams params,
MessageInfo message,
CancellationToken token,
) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
var filePath = pathOfDoc(params.textDocument);
return filePath.mapResult((filePath) async {
var unitResult = await server.getResolvedUnit(filePath);
if (unitResult == null) {
return success(null);
}
var lineInfo = unitResult.lineInfo;
// Compute the ranges for which we will provide values. We produce two
// ranges here because for some kinds of variables (simple values) it's
// convenient to see them on an `if` statement on the same line that
// hasn't executed yet. However, we should avoid evaluating getters which
// may have side effects if they haven't executed previously, because this
// may change state in a way that's less obvious.
var visibleRange = params.range;
var stoppedLocation = params.context.stoppedLocation;
var rangeAlreadyExecuted = Range(
start: visibleRange.start,
end: stoppedLocation.end,
);
var rangeIncludingCurrentLine = Range(
start: visibleRange.start,
end: Position(line: stoppedLocation.end.line + 1, character: 0),
);
var stoppedOffset = toOffset(lineInfo, stoppedLocation.end);
return stoppedOffset.mapResult((stoppedOffset) async {
// Find the function that is executing. We will only show values for
// this single function expression.
var node = unitResult.unit.nodeCovering(offset: stoppedOffset);
var function = node?.thisOrAncestorMatching(
(node) => node is FunctionExpression || node is MethodDeclaration,
);
if (function == null) {
return success(null);
}
var collector = _InlineValueCollector(
lineInfo,
rangeAlreadyExecuted: rangeAlreadyExecuted,
rangeIncludingCurrentLine: rangeIncludingCurrentLine,
);
var visitor = _InlineValueVisitor(
server.lspClientConfiguration,
collector,
function,
);
function.accept(visitor);
return success(collector.values.values.toList());
});
});
}
}
class InlineValueRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
InlineValueRegistrations(super.info);
@override
ToJsonable? get options =>
TextDocumentRegistrationOptions(documentSelector: dartFiles);
@override
Method get registrationMethod => Method.textDocument_inlineValue;
@override
StaticOptions get staticOptions => Either3.t1(true);
@override
bool get supportsDynamic => clientDynamic.inlineValue;
}
/// Collects inline values, keeping only the most relevant where an element
/// is recorded multiple times.
class _InlineValueCollector {
/// A map of elements and their inline value.
final Map<analyzer.Element, InlineValue> values = {};
/// The range for which simple inline values should be returned.
///
/// This should be approximately the range of the visible code on screen up to
/// the point of execution and including the current line.
final Range rangeIncludingCurrentLine;
/// The range for which complex inline values (such as getters) should be
/// returned.
///
/// This should be approximately the range of the visible code on screen up to
/// the point of execution.
final Range rangeAlreadyExecuted;
/// A [LineInfo] used to convert offsets to lines/columns for comparing to
/// [applicableRange].
final LineInfo lineInfo;
_InlineValueCollector(
this.lineInfo, {
required this.rangeAlreadyExecuted,
required this.rangeIncludingCurrentLine,
});
/// Records an expression inline value for [element] with [offset]/[length].
///
/// 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(analyzer.Element? element, int offset, int length) {
assert(offset >= 0);
assert(length > 0);
if (element == null) return;
var range = toRange(lineInfo, offset, length);
// Don't record anything outside of the visible range (excluding next line).
if (!range.intersects(rangeAlreadyExecuted)) {
return;
}
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(analyzer.Element? element, int offset, int length) {
assert(offset >= 0);
assert(length > 0);
if (element == null || element.isWildcardVariable) return;
var range = toRange(lineInfo, offset, length);
// Don't record anything outside of the visible range (including next line).
if (!range.intersects(rangeIncludingCurrentLine)) {
return;
}
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,
);
}
/// Returns whether [element] is something that should never be eagerly
/// evaluated because of potential side-effects (such as `iterable.length`).
bool _isExcludedElement(analyzer.Element element) {
return switch (element) {
analyzer.VariableElement() => _isExcludedType(element.type),
analyzer.GetterElement() => _isExcludedType(element.returnType),
_ => false,
};
}
/// Returns whether [type] is something that should never be eagerly
/// evaluated because of potential side-effects (such as `iterable.length`).
bool _isExcludedType(DartType? type) {
if (type == null) {
return false;
}
return type.isDartCoreIterable ||
type.isDartAsyncFuture ||
type.isDartAsyncFutureOr ||
type.isDartAsyncStream;
}
/// 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, analyzer.Element element) {
// Don't create values for any elements that are excluded types.
if (_isExcludedElement(element)) {
return;
}
var range = _getRange(value);
// We only want to show each variable once, so keep only the one furthest
// into the source (closest to the execution pointer).
if (values[element] case var existingValue?) {
var existingPosition = _getRange(existingValue).start;
if (existingPosition.isAfterOrEqual(range.start)) {
return;
}
}
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 AstNode rootNode;
_InlineValueVisitor(this.clientConfiguration, this.collector, this.rootNode);
bool get experimentalInlineValuesProperties =>
clientConfiguration.global.experimentalInlineValuesProperties;
@override
void visitFormalParameter(FormalParameter node) {
var name = node.name;
if (name != null) {
collector.recordVariableLookup(
node.declaredFragment?.element,
name.offset,
name.length,
);
}
super.visitFormalParameter(node);
}
@override
void visitFunctionExpression(FunctionExpression node) {
// Don't descend into nested functions.
if (node != rootNode) {
return;
}
super.visitFunctionExpression(node);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
if (experimentalInlineValuesProperties) {
// Don't create values for excluded types or access of their properties.
if (collector._isExcludedType(node.prefix.staticType)) {
return;
}
var parent = node.parent;
// Never produce values for the left side of a property access.
var isTarget = parent is PropertyAccess && node == parent.realTarget;
// Never produce values for obvious enum getters (this includes `values`).
var isEnumGetter =
node.element is analyzer.GetterElement &&
node.element?.enclosingElement2 is analyzer.EnumElement;
if (!isTarget && !isEnumGetter) {
collector.recordExpression(node.element, node.offset, node.length);
}
}
super.visitPrefixedIdentifier(node);
}
@override
void visitPropertyAccess(PropertyAccess node) {
var target = node.target;
if (experimentalInlineValuesProperties && target is Identifier) {
// Don't create values for excluded types or access of their properties.
if (collector._isExcludedType(target.staticType)) {
return;
}
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 analyzer.LocalVariableElement(name3: _?):
case analyzer.FormalParameterElement():
collector.recordVariableLookup(
node.element,
node.offset,
node.length,
);
}
}
super.visitSimpleIdentifier(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
var name = node.name;
collector.recordVariableLookup(
node.declaredElement2,
name.offset,
name.length,
);
super.visitVariableDeclaration(node);
}
}