a8314dac74
Fixes https://github.com/dart-lang/sdk/issues/55986 Change-Id: Icc7e6dbb4272613e838b0bd2ef9ac4764dd068f1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/373200 Commit-Queue: Jens Johansen <jensj@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com>
1215 lines
38 KiB
Dart
1215 lines
38 KiB
Dart
// Copyright (c) 2023, 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:kernel/ast.dart';
|
|
|
|
/// Dart scope
|
|
///
|
|
/// Provides information about symbols available inside a dart scope.
|
|
class DartScope {
|
|
final Library library;
|
|
final Class? cls;
|
|
final Member? member;
|
|
final bool isStatic;
|
|
final Map<String, DartType> definitions;
|
|
final List<TypeParameter> typeParameters;
|
|
|
|
DartScope(this.library, this.cls, this.member, this.definitions,
|
|
this.typeParameters)
|
|
: isStatic = member is Procedure ? member.isStatic : false;
|
|
|
|
@override
|
|
String toString() {
|
|
return '''DartScope {
|
|
Library: ${library.importUri},
|
|
Class: ${cls?.name},
|
|
Procedure: $member,
|
|
isStatic: $isStatic,
|
|
Scope: $definitions,
|
|
typeParameters: $typeParameters
|
|
}
|
|
''';
|
|
}
|
|
}
|
|
|
|
/// Dart scope
|
|
///
|
|
/// Provides information about symbols available inside a dart scope.
|
|
class DartScope2 {
|
|
final TreeNode node;
|
|
final Library library;
|
|
final Class? cls;
|
|
final Member? member;
|
|
final bool isStatic;
|
|
final Map<String, VariableDeclaration> definitions;
|
|
final List<TypeParameter> typeParameters;
|
|
|
|
DartScope2(this.node, this.library, this.cls, this.member, this.definitions,
|
|
this.typeParameters)
|
|
: isStatic = member is Procedure ? member.isStatic : false;
|
|
|
|
@override
|
|
String toString() {
|
|
return '''DartScope {
|
|
Library: ${library.importUri},
|
|
Class: ${cls?.name},
|
|
Procedure: $member,
|
|
isStatic: $isStatic,
|
|
Scope: $definitions,
|
|
typeParameters: $typeParameters
|
|
}
|
|
''';
|
|
}
|
|
}
|
|
|
|
/// DartScopeBuilder finds dart scope information for a location.
|
|
///
|
|
/// Find all definitions in scope at a given 1-based [line] and [column]:
|
|
///
|
|
/// - library
|
|
/// - class
|
|
/// - locals
|
|
/// - formals
|
|
/// - captured variables (for closures)
|
|
class DartScopeBuilder extends VisitorDefault<void> with VisitorVoidMixin {
|
|
final Component _component;
|
|
final int _line;
|
|
final int _column;
|
|
|
|
Library? _library;
|
|
Class? _cls;
|
|
Member? _member;
|
|
int _offset = -1;
|
|
|
|
final List<FunctionNode> _functions = [];
|
|
final Map<String, DartType> _definitions = {};
|
|
final List<TypeParameter> _typeParameters = [];
|
|
|
|
DartScopeBuilder._(this._component, this._line, this._column);
|
|
|
|
static DartScope? findScope(
|
|
Component component, Library library, int line, int column) {
|
|
DartScopeBuilder builder = DartScopeBuilder._(component, line, column);
|
|
library.accept(builder);
|
|
return builder.build();
|
|
}
|
|
|
|
DartScope? build() {
|
|
if (_offset < 0 || _library == null) return null;
|
|
|
|
return DartScope(_library!, _cls, _member, _definitions, _typeParameters);
|
|
}
|
|
|
|
@override
|
|
void defaultTreeNode(Node node) {
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitLibrary(Library library) {
|
|
_library = library;
|
|
_offset = 0;
|
|
if (_line > 0) {
|
|
_offset = _component.getOffset(_library!.fileUri, _line, _column);
|
|
}
|
|
|
|
// Exit early if the evaluation offset is not found.
|
|
// Note: the complete scope is not found in this case,
|
|
// so the expression compiler will report an error.
|
|
if (_offset >= 0) super.visitLibrary(library);
|
|
}
|
|
|
|
@override
|
|
void visitClass(Class cls) {
|
|
if (_scopeContainsOffset(cls.fileOffset, cls.fileEndOffset, _offset)) {
|
|
_cls = cls;
|
|
_typeParameters.addAll(cls.typeParameters);
|
|
|
|
super.visitClass(cls);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void defaultMember(Member m) {
|
|
if (_scopeContainsOffset(m.fileOffset, m.fileEndOffset, _offset)) {
|
|
_member = m;
|
|
|
|
super.defaultMember(m);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitFunctionNode(FunctionNode fun) {
|
|
if (_scopeContainsOffset(fun.fileOffset, fun.fileEndOffset, _offset)) {
|
|
_functions.add(fun);
|
|
_typeParameters.addAll(fun.typeParameters);
|
|
|
|
super.visitFunctionNode(fun);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitVariableDeclaration(VariableDeclaration decl) {
|
|
String? name = decl.name;
|
|
// Collect locals and formals appearing before current breakpoint.
|
|
// Note that we include variables with no offset because the offset
|
|
// is not set in many cases in generated code, so omitting them would
|
|
// make expression evaluation fail in too many cases.
|
|
// Issue: https://github.com/dart-lang/sdk/issues/43966
|
|
//
|
|
// A null name signals that the variable was synthetically introduced by the
|
|
// compiler so they are skipped.
|
|
if ((decl.fileOffset < 0 || decl.fileOffset < _offset) && name != null) {
|
|
_definitions[name] = decl.type;
|
|
}
|
|
super.visitVariableDeclaration(decl);
|
|
}
|
|
|
|
@override
|
|
void visitBlock(Block block) {
|
|
int fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
|
|
if (_scopeContainsOffset(block.fileOffset, fileEndOffset, _offset)) {
|
|
super.visitBlock(block);
|
|
}
|
|
}
|
|
|
|
bool _scopeContainsOffset(int startOffset, int endOffset, int offset) {
|
|
if (offset < 0 || startOffset < 0 || endOffset < 0) {
|
|
return false;
|
|
}
|
|
return startOffset <= offset && offset <= endOffset;
|
|
}
|
|
}
|
|
|
|
/// File end offset calculator.
|
|
///
|
|
/// Helps calculate file end offsets for nodes with internal scope
|
|
/// that do not have .fileEndOffset field.
|
|
///
|
|
/// For example - [Block]
|
|
class FileEndOffsetCalculator extends VisitorDefault<int?>
|
|
with VisitorNullMixin<int> {
|
|
static const int noOffset = -1;
|
|
|
|
final int _startOffset;
|
|
final TreeNode _root;
|
|
final TreeNode _original;
|
|
|
|
int _endOffset = noOffset;
|
|
|
|
/// Create calculator for a scoping node with no .fileEndOffset.
|
|
///
|
|
/// [_root] is the parent of the scoping node.
|
|
/// [_startOffset] is the start offset of the scoping node.
|
|
FileEndOffsetCalculator._(this._root, this._original)
|
|
: _startOffset = _original.fileOffset;
|
|
|
|
/// Calculate file end offset for a scoping node.
|
|
///
|
|
/// This calculator finds the first node in the ancestor chain that
|
|
/// can give such information for a given [node], i.e. satisfies one
|
|
/// of the following conditions:
|
|
///
|
|
/// - a node with a greater start offset that is a child of the
|
|
/// closest ancestor. The start offset of this child is used as a
|
|
/// file end offset of the [node].
|
|
///
|
|
/// - the closest ancestor with .fileEndOffset information. The file
|
|
/// end offset of the ancestor is used as the file end offset of
|
|
/// the [node.]
|
|
///
|
|
/// If none found, return [noOffset].
|
|
static int calculateEndOffset(TreeNode node) {
|
|
for (TreeNode? n = node.parent; n != null; n = n.parent) {
|
|
FileEndOffsetCalculator calculator = FileEndOffsetCalculator._(n, node);
|
|
int? offset = n.accept(calculator);
|
|
if (offset != noOffset) return offset!;
|
|
}
|
|
return noOffset;
|
|
}
|
|
|
|
@override
|
|
int defaultTreeNode(TreeNode node) {
|
|
if (node == _original) return _endOffset;
|
|
if (node == _root) {
|
|
node.visitChildren(this);
|
|
if (_endOffset != noOffset) return _endOffset;
|
|
return _endOffsetForNode(node);
|
|
}
|
|
// Skip synthesized variables as they could have offsets
|
|
// from later code (in case they are hoisted, for example).
|
|
if ((node is! VariableDeclaration || !node.isSynthesized) &&
|
|
_endOffset == noOffset &&
|
|
node.fileOffset > _startOffset) {
|
|
_endOffset = node.fileOffset;
|
|
}
|
|
return _endOffset;
|
|
}
|
|
|
|
static int _endOffsetForNode(TreeNode node) {
|
|
if (node is Class) return node.fileEndOffset;
|
|
if (node is Constructor) return node.fileEndOffset;
|
|
if (node is Procedure) return node.fileEndOffset;
|
|
if (node is Field) return node.fileEndOffset;
|
|
if (node is FunctionNode) return node.fileEndOffset;
|
|
return noOffset;
|
|
}
|
|
}
|
|
|
|
class DartScopeBuilder2 extends VisitorDefault<void> with VisitorVoidMixin {
|
|
final Library _library;
|
|
final Uri _scriptUri;
|
|
final int _offset;
|
|
final List<DartScope2> findScopes = [];
|
|
final Set<int> foundOffsets = {};
|
|
|
|
final Set<VariableDeclaration> hoistedUnwritten = {};
|
|
final List<List<VariableDeclaration>> scopes = [];
|
|
final List<List<TypeParameter>> typeParameterScopes = [];
|
|
|
|
Class? _currentCls = null;
|
|
Member? _currentMember = null;
|
|
|
|
bool checkClasses = true;
|
|
Uri _currentUri;
|
|
|
|
DartScopeBuilder2._(this._library, this._scriptUri, this._offset)
|
|
: _currentUri = _library.fileUri;
|
|
|
|
void clearScope() {
|
|
scopes.clear();
|
|
hoistedUnwritten.clear();
|
|
}
|
|
|
|
void addFound(TreeNode node) {
|
|
Map<String, VariableDeclaration> definitions = {};
|
|
for (List<VariableDeclaration> scope in scopes) {
|
|
for (VariableDeclaration decl in scope) {
|
|
String? name = decl.name;
|
|
if (name != null &&
|
|
!decl.isSynthesized &&
|
|
!hoistedUnwritten.contains(decl)) {
|
|
definitions[name] = decl;
|
|
}
|
|
}
|
|
}
|
|
// TODO(jensj): If the current member is static and we're in a class we have
|
|
// to skip the typeParameters from the class.
|
|
List<TypeParameter> typeParameters = [];
|
|
for (List<TypeParameter> typeParameterScope in typeParameterScopes) {
|
|
typeParameters.addAll(typeParameterScope);
|
|
}
|
|
DartScope2 findScope = new DartScope2(node, _library, _currentCls,
|
|
_currentMember, definitions, typeParameters);
|
|
findScopes.add(findScope);
|
|
}
|
|
|
|
@override
|
|
void defaultDartType(DartType node) {
|
|
return;
|
|
}
|
|
|
|
@override
|
|
void defaultTreeNode(TreeNode node) {
|
|
Uri prevUri = _currentUri;
|
|
if (node is FileUriNode) {
|
|
_currentUri = node.fileUri;
|
|
}
|
|
_checkOffset(node);
|
|
node.visitChildren(this);
|
|
_currentUri = prevUri;
|
|
}
|
|
|
|
@override
|
|
void visitAssertBlock(AssertBlock node) {
|
|
scopes.add([]);
|
|
super.visitAssertBlock(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitBlock(Block node) {
|
|
scopes.add([]);
|
|
|
|
_checkOffset(node);
|
|
|
|
bool shouldSkip;
|
|
|
|
// See also the test [DillBlockChecker] which checks that we can do this
|
|
// pruning!
|
|
if (_currentUri != _scriptUri) {
|
|
shouldSkip = true;
|
|
} else if (node.parent is ForInStatement) {
|
|
// E.g. dart2js implicit cast in for-in loop
|
|
shouldSkip = false;
|
|
} else if (node.parent?.parent is ForStatement) {
|
|
// A vm transformation turns
|
|
// `for (var foo in bar) {}`
|
|
// into
|
|
// `for(;iterator.moveNext; ) { var foo = iterator.current; {} }`
|
|
// where the block directly containing `foo` has the original blocks
|
|
// offset, i.e. after the variable declaration, but it still contain
|
|
// it. So we pretend it has no offsets.
|
|
shouldSkip = false;
|
|
} else if (node.fileOffset >= 0 &&
|
|
node.fileEndOffset >= 0 &&
|
|
node.fileOffset != node.fileEndOffset) {
|
|
if (_offset < node.fileOffset || _offset > node.fileEndOffset) {
|
|
// Not contained in the block.
|
|
shouldSkip = true;
|
|
} else {
|
|
// Contained in the block.
|
|
shouldSkip = false;
|
|
}
|
|
} else {
|
|
// The block doesn't have valid offsets.
|
|
shouldSkip = false;
|
|
}
|
|
|
|
if (!shouldSkip) {
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitBlockExpression(BlockExpression node) {
|
|
scopes.add([]);
|
|
super.visitBlockExpression(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitCatch(Catch node) {
|
|
scopes.add([]);
|
|
super.visitCatch(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitClass(Class node) {
|
|
if (!checkClasses) {
|
|
return;
|
|
}
|
|
_currentCls = node;
|
|
typeParameterScopes.add([...node.typeParameters]);
|
|
scopes.add([]);
|
|
super.visitClass(node);
|
|
clearScope();
|
|
typeParameterScopes.removeLast();
|
|
assert(typeParameterScopes.isEmpty);
|
|
_currentCls = null;
|
|
}
|
|
|
|
@override
|
|
void visitConstructor(Constructor node) {
|
|
Uri prevUri = _currentUri;
|
|
_currentUri = node.fileUri;
|
|
|
|
_currentMember = node;
|
|
clearScope();
|
|
scopes.add([]);
|
|
|
|
_checkOffset(node);
|
|
|
|
// The constructor is special in that the parameters from the contained
|
|
// function node is in scope in the initializers.
|
|
node.function.accept(this);
|
|
for (VariableDeclaration param in node.function.positionalParameters) {
|
|
scopes.last.add(param);
|
|
}
|
|
for (VariableDeclaration param in node.function.namedParameters) {
|
|
scopes.last.add(param);
|
|
}
|
|
for (Initializer initializer in node.initializers) {
|
|
initializer.accept(this);
|
|
}
|
|
|
|
clearScope();
|
|
_currentMember = null;
|
|
_currentUri = prevUri;
|
|
}
|
|
|
|
@override
|
|
void visitExtension(Extension node) {
|
|
typeParameterScopes.add([...node.typeParameters]);
|
|
super.visitExtension(node);
|
|
typeParameterScopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) {
|
|
typeParameterScopes.add([...node.typeParameters]);
|
|
super.visitExtensionTypeDeclaration(node);
|
|
typeParameterScopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitField(Field node) {
|
|
_currentMember = node;
|
|
clearScope();
|
|
scopes.add([]);
|
|
super.visitField(node);
|
|
clearScope();
|
|
_currentMember = null;
|
|
}
|
|
|
|
@override
|
|
void visitForInStatement(ForInStatement node) {
|
|
scopes.add([]);
|
|
super.visitForInStatement(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitForStatement(ForStatement node) {
|
|
scopes.add([]);
|
|
super.visitForStatement(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitFunctionNode(FunctionNode node) {
|
|
typeParameterScopes.add([...node.typeParameters]);
|
|
scopes.add([]);
|
|
super.visitFunctionNode(node);
|
|
scopes.removeLast();
|
|
typeParameterScopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitLet(Let node) {
|
|
scopes.add([]);
|
|
super.visitLet(node);
|
|
scopes.removeLast();
|
|
}
|
|
|
|
@override
|
|
void visitLibrary(Library node) {
|
|
scopes.add([]);
|
|
super.visitLibrary(node);
|
|
clearScope();
|
|
}
|
|
|
|
@override
|
|
void visitProcedure(Procedure node) {
|
|
_currentMember = node;
|
|
clearScope();
|
|
scopes.add([]);
|
|
super.visitProcedure(node);
|
|
clearScope();
|
|
_currentMember = null;
|
|
}
|
|
|
|
@override
|
|
void visitTypedef(Typedef node) {
|
|
clearScope();
|
|
scopes.add([]);
|
|
typeParameterScopes.add([...node.typeParameters]);
|
|
super.visitTypedef(node);
|
|
typeParameterScopes.removeLast();
|
|
clearScope();
|
|
}
|
|
|
|
@override
|
|
void visitVariableSet(VariableSet node) {
|
|
super.visitVariableSet(node);
|
|
if (node.variable.isHoisted) {
|
|
hoistedUnwritten.remove(node.variable);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitVariableDeclaration(VariableDeclaration node) {
|
|
if (node.isHoisted) hoistedUnwritten.add(node);
|
|
super.visitVariableDeclaration(node);
|
|
// Declare it after.
|
|
scopes.last.add(node);
|
|
}
|
|
|
|
void _checkOffset(TreeNode node) {
|
|
if (_currentUri == _scriptUri) {
|
|
foundOffsets.add(node.fileOffset);
|
|
if (node.fileOffset == _offset) {
|
|
addFound(node);
|
|
} else {
|
|
List<int>? allOffsets = node.fileOffsetsIfMultiple;
|
|
if (allOffsets != null) {
|
|
for (final int offset in allOffsets) {
|
|
foundOffsets.add(offset);
|
|
if (offset == _offset) {
|
|
addFound(node);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static DartScope findScopeFromOffsetAndClass(
|
|
Library library, Uri scriptUri, Class? cls, int offset) {
|
|
DartScopeBuilder2 data = _raw(library, scriptUri, cls, offset);
|
|
if (data.findScopes.isEmpty) {
|
|
int? closestMatchingOrSmallerOffset =
|
|
_findClosestMatchingOrSmallerOffset(data, offset);
|
|
if (closestMatchingOrSmallerOffset != null) {
|
|
offset = closestMatchingOrSmallerOffset;
|
|
data = _raw(library, scriptUri, cls, offset);
|
|
}
|
|
}
|
|
return _findScopePick(data.findScopes, library, cls, offset);
|
|
}
|
|
|
|
static int? _findClosestMatchingOrSmallerOffset(
|
|
DartScopeBuilder2 data, int offset) {
|
|
List<int> foundOffsets = data.foundOffsets.toList()..sort();
|
|
if (foundOffsets.isEmpty) return null;
|
|
int low = 0;
|
|
int high = foundOffsets.length - 1;
|
|
while (low < high) {
|
|
int mid = high - ((high - low) >> 1); // Get middle, rounding up.
|
|
int pivot = foundOffsets[mid];
|
|
if (pivot <= offset) {
|
|
low = mid;
|
|
} else {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
int result = foundOffsets[low];
|
|
if (result < 0) return null;
|
|
return result;
|
|
}
|
|
|
|
static DartScope _findScopePick(
|
|
List<DartScope2> scopes, Library library, Class? cls, int offset) {
|
|
DartScope2 scope;
|
|
if (scopes.length == 0) {
|
|
// This shouldn't happen.
|
|
return new DartScope(library, cls, null, {}, []);
|
|
} else if (scopes.length == 1) {
|
|
scope = scopes.single;
|
|
} else {
|
|
List<DartScope2> filteredScopes = _filterAll(scopes, library, offset);
|
|
if (filteredScopes.length == 0) {
|
|
// This shouldn't happen.
|
|
filteredScopes = scopes;
|
|
}
|
|
if (filteredScopes.length == 1) {
|
|
scope = filteredScopes.single;
|
|
} else {
|
|
// TODO(jensj): This shouldn't happen, but inevitably will.
|
|
// When it does, what should we do?
|
|
scope = scopes.last;
|
|
}
|
|
}
|
|
|
|
Map<String, DartType> definitions = {};
|
|
for (MapEntry<String, VariableDeclaration> entry
|
|
in scope.definitions.entries) {
|
|
definitions[entry.key] = entry.value.type;
|
|
}
|
|
return new DartScope(scope.library, scope.cls, scope.member, definitions,
|
|
scope.typeParameters);
|
|
}
|
|
|
|
static DartScope findScopeFromOffset(
|
|
Library library, Uri scriptUri, int offset) {
|
|
DartScopeBuilder2 data = _rawNoClass(library, scriptUri, offset);
|
|
if (data.findScopes.isEmpty) {
|
|
int? closestMatchingOrSmallerOffset =
|
|
_findClosestMatchingOrSmallerOffset(data, offset);
|
|
if (closestMatchingOrSmallerOffset != null) {
|
|
offset = closestMatchingOrSmallerOffset;
|
|
data = _rawNoClass(library, scriptUri, offset);
|
|
}
|
|
}
|
|
return _findScopePick(data.findScopes, library, null, offset);
|
|
}
|
|
|
|
static List<DartScope2> _filterAll(
|
|
List<DartScope2> rawScopes, Library library, int offset) {
|
|
List<DartScope2> firstFilteredScopes =
|
|
_filterScopesWithArtificialNodes(rawScopes, library);
|
|
if (firstFilteredScopes.isEmpty) {
|
|
return rawScopes;
|
|
}
|
|
if (_allHaveTheSameDefinitions(firstFilteredScopes)) {
|
|
return [firstFilteredScopes.first];
|
|
}
|
|
List<DartScope2> filteredScopes =
|
|
_filter(firstFilteredScopes, library, offset);
|
|
if (_allHaveTheSameDefinitions(filteredScopes)) {
|
|
return [filteredScopes.first];
|
|
}
|
|
return filteredScopes;
|
|
}
|
|
|
|
static List<DartScope2> filterAllForTesting(
|
|
List<DartScope2> rawScopes, Library library, int offset) {
|
|
return _filterAll(rawScopes, library, offset);
|
|
}
|
|
|
|
static List<DartScope2> _filterScopesWithArtificialNodes(
|
|
List<DartScope2> unfilteredScopes, Library library) {
|
|
Set<Member> skipMembers = {};
|
|
for (Extension node in library.extensions) {
|
|
for (ExtensionMemberDescriptor memberDescriptor
|
|
in node.memberDescriptors) {
|
|
// The tear off procedures have two enclosing function nodes with the
|
|
// same offsets, but with (possibly) different type parameters.
|
|
// Skip them.
|
|
Member? skip = memberDescriptor.tearOffReference?.asMember;
|
|
if (skip != null) skipMembers.add(skip);
|
|
}
|
|
}
|
|
|
|
List<DartScope2> filtered = [];
|
|
for (DartScope2 node in unfilteredScopes) {
|
|
Member? member = node.member;
|
|
if (skipMembers.contains(member)) {
|
|
// Skip these.
|
|
continue;
|
|
}
|
|
if (member is Field) {
|
|
if (member.isInternalImplementation) {
|
|
// E.g. synthesized fields added by late lowering. Skip those.
|
|
continue;
|
|
}
|
|
} else if (member is Procedure) {
|
|
if (member.isSynthetic) {
|
|
// Skip synthetic procedures.
|
|
continue;
|
|
}
|
|
}
|
|
filtered.add(node);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
static bool _allHaveTheSameDefinitions(List<DartScope2> scopes) {
|
|
if (scopes.isEmpty) return false;
|
|
Map<String, VariableDeclaration> definitions = scopes.first.definitions;
|
|
for (int i = 1; i < scopes.length; i++) {
|
|
DartScope2 scope = scopes[i];
|
|
if (scope.definitions.length != definitions.length) return false;
|
|
for (MapEntry<String, VariableDeclaration> entry
|
|
in scope.definitions.entries) {
|
|
VariableDeclaration? existing = definitions[entry.key];
|
|
if (existing == null) {
|
|
return false;
|
|
} else {
|
|
if (existing != entry.value) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static List<DartScope2> _filter(
|
|
List<DartScope2> unfilteredScopes, Library library, int offset) {
|
|
List<DartScope2> filtered = unfilteredScopes.toList();
|
|
List<DartScope2> withoutEndOffset = [];
|
|
for (DartScope2 scope in unfilteredScopes) {
|
|
TreeNode? node = scope.node;
|
|
// Possibly filter out nodes that was only included because their end
|
|
// offset matched the offset we we're looking for.
|
|
if (node is Member) {
|
|
if (offset != node.fileEndOffset) {
|
|
withoutEndOffset.add(scope);
|
|
}
|
|
} else if (node is FunctionNode) {
|
|
if (offset != node.fileEndOffset) {
|
|
withoutEndOffset.add(scope);
|
|
}
|
|
} else if (node is Block) {
|
|
if (offset != node.fileEndOffset) {
|
|
withoutEndOffset.add(scope);
|
|
}
|
|
} else {
|
|
withoutEndOffset.add(scope);
|
|
}
|
|
}
|
|
|
|
if (withoutEndOffset.length == 1) {
|
|
return withoutEndOffset;
|
|
} else if (withoutEndOffset.isEmpty) {
|
|
// They're all on the end offset. Just return the last one.
|
|
// E.g.
|
|
// ```
|
|
// static makeErrorHandler(_EventSink controller) =>
|
|
// (Object e, StackTrace s) {
|
|
// controller._addError(e, s);
|
|
// controller._close();
|
|
// };
|
|
// ```
|
|
// have both the Procedure, the first FunctionNode and the second
|
|
// FunctionNode with the same end offset.
|
|
return [filtered.last];
|
|
} else {
|
|
filtered = withoutEndOffset;
|
|
}
|
|
|
|
if ((filtered.length == 2 &&
|
|
filtered[0].node is ForInStatement &&
|
|
filtered[1].node is Block) ||
|
|
(filtered.length == 3 &&
|
|
filtered[0].node is ForInStatement &&
|
|
filtered[1].node is LabeledStatement &&
|
|
filtered[2].node is Block)) {
|
|
// In the below code the for in statement was included on the brace
|
|
// because of the body offset, but as we have the block that's what we
|
|
// should use, so we remove the ForInStatement.
|
|
// ```
|
|
// String? name = node.name;
|
|
// // [...]
|
|
// for (String name in combinator.names) { // <- this brace
|
|
// // whatnot
|
|
// }
|
|
// ```
|
|
return [filtered.last];
|
|
}
|
|
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is ForInStatement &&
|
|
filtered[1].node is YieldStatement) {
|
|
// E.g.
|
|
// ```
|
|
// for (var key in keys) yield MapEntry<K, V>(key, this[key] as dynamic);
|
|
// ```
|
|
// where the ForIn and the yield has the same (body) offset.
|
|
return [filtered[1]];
|
|
}
|
|
|
|
if (filtered.length > 2 &&
|
|
filtered.length < 5 &&
|
|
filtered[0].node is ForInStatement &&
|
|
filtered[1].node is ExpressionStatement) {
|
|
// E.g.
|
|
// ```
|
|
// for (var entry in entries) this[entry.key] = entry.value;
|
|
// ```
|
|
return [filtered.last];
|
|
}
|
|
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is FunctionNode &&
|
|
filtered[1].node is Block) {
|
|
// Remove the function node one(s) --- assume we're on the block
|
|
// to remove some uncertainty in for instance these situations:
|
|
// ```
|
|
// void foo(String? s) {
|
|
// if (s == null) return;
|
|
// bar(String s) {
|
|
// // whatever
|
|
// } // <- if the position is at this brace.
|
|
// bar(s);
|
|
// }
|
|
// ```
|
|
// and
|
|
// ```
|
|
// void bla() {
|
|
// void foo(String foo) {
|
|
// // Here "foo" is a string.
|
|
// } // <- if the position is at this brace.
|
|
// }
|
|
// ```
|
|
// In both situations we have two variables with the same name but
|
|
// different types, depending on if the positionals from the function node
|
|
// is "on" or not.
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is FieldInitializer) {
|
|
// Pick the FieldInitializer (i.e. have the variable in scope).
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is VariableGet &&
|
|
filtered[1].node.parent?.parent is SuperInitializer) {
|
|
// E.g. `Foo(super.bar)`.
|
|
// Pick the VariableGet (with all variables in scope --- this is not
|
|
// great if we can actually stop before, but can we?).
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is Constructor &&
|
|
filtered[1].node is SuperInitializer) {
|
|
// Pick the SuperInitializer (i.e. have any variables in scope).
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is VariableGet &&
|
|
filtered[0].node.parent?.parent is Procedure &&
|
|
(filtered[0].node.parent?.parent as Procedure).isRedirectingFactory) {
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is FunctionNode &&
|
|
filtered[1].node is InvocationExpression &&
|
|
filtered[0].node.parent is Procedure &&
|
|
(filtered[0].node.parent as Procedure).isRedirectingFactory) {
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length > 1 &&
|
|
filtered[0].node is Class &&
|
|
(filtered[0].node as Class).isEnum) {
|
|
return [filtered[0]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is VariableGet &&
|
|
filtered[0].node.parent?.parent is Constructor &&
|
|
filtered[1].node.parent?.parent is Arguments) {
|
|
// E.g. `super.localId`.
|
|
return [filtered[1]];
|
|
}
|
|
if (filtered.length == 2 &&
|
|
filtered[0].node is IfStatement &&
|
|
filtered[1].node is Block) {
|
|
return [filtered[0]];
|
|
}
|
|
if (filtered.length > 1 &&
|
|
filtered[0].node is InstanceGet &&
|
|
(filtered[0].node as InstanceGet).receiver is VariableGet &&
|
|
((filtered[0].node as InstanceGet).receiver as VariableGet)
|
|
.variable
|
|
.isSynthesized) {
|
|
// E.g. `await for (var fileEntity in stream) { [...] }` that has become
|
|
// FileSystemEntity fileEntity = :for-iterator.{_StreamIterator.current};
|
|
// {
|
|
// [...]
|
|
// }
|
|
// where the getting of the iterator and the block has the same offset,
|
|
// pointing to the start brace after `fileEntity` was defined in the
|
|
// source. We remove the InstanceGet.
|
|
filtered.removeAt(0);
|
|
}
|
|
if (filtered.length == 1) return filtered;
|
|
|
|
DartScope2? lookup;
|
|
|
|
// TODO(jensj): Look into rewriting the matching to use patterns.
|
|
// See also
|
|
// https://dart-review.googlesource.com/c/sdk/+/338840/8/pkg/kernel/lib/dart_scope_calculator.dart#924
|
|
lookup = _looksLikeVmForInTransformation(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
lookup = _looksLikeVmFfiTransformation(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
lookup = _looksLikeLateLoweredField(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
lookup = _looksLikePatternMatching(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
lookup = _looksLikeLateLoweredLocal(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
lookup = _looksLikeThrowPatternMatchingError(filtered);
|
|
if (lookup != null) return [lookup];
|
|
|
|
return filtered;
|
|
}
|
|
|
|
static DartScope2? _looksLikeVmForInTransformation(
|
|
List<DartScope2> filtered) {
|
|
// The VM has a transformation where
|
|
// ```
|
|
// void foo(String x) {
|
|
// for(dynamic x in bar()) {
|
|
// if (x == null) continue;
|
|
// print(x);
|
|
// }
|
|
// }
|
|
// List bar() => [1, 2, 3];
|
|
// ```
|
|
// is transformed into something like
|
|
// ```
|
|
// {
|
|
// synthesized core::Iterator<dynamic> :sync-for-iterator =
|
|
// self::bar().{core::Iterable::iterator}{core::Iterator<dynamic>};
|
|
// for (;
|
|
// :sync-for-iterator.{core::Iterator::moveNext}(){() → core::bool}; )
|
|
// { // <- this block
|
|
// dynamic x = :sync-for-iterator.
|
|
// {core::Iterator::current}{dynamic}; // <- the initializer
|
|
// #L1: // <- this label
|
|
// { // <- this block
|
|
// if(x == null)
|
|
// break #L1;
|
|
// core::print(x);
|
|
// }
|
|
// }
|
|
// }
|
|
// ```
|
|
// where I've marked the 4 places where the offset is the same.
|
|
// In the first 2 cases the "x" will be String, in the other 2 the "x" will
|
|
// be dynamic.
|
|
// Try to filter this so that we say we're where "x" is dynamic.
|
|
if (filtered.length >= 3 &&
|
|
filtered.first.node is Block &&
|
|
filtered[1].node is InstanceGet) {
|
|
Block firstBlock = filtered.first.node as Block;
|
|
InstanceGet instanceGet = filtered[1].node as InstanceGet;
|
|
Expression receiver = instanceGet.receiver;
|
|
if (firstBlock.parent is ForStatement &&
|
|
receiver is VariableGet &&
|
|
receiver.variable.isSynthesized &&
|
|
receiver.variable.name == ":sync-for-iterator") {
|
|
// Matches the case. Return the last block.
|
|
return filtered.last;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static DartScope2? _looksLikeVmFfiTransformation(List<DartScope2> filtered) {
|
|
if (filtered.length > 4 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[2].node is Field &&
|
|
(filtered[2].node as Field).name.text.endsWith("\$FfiNative\$Ptr")) {
|
|
return filtered[0];
|
|
} else if (filtered.length > 4 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[2].node is Procedure &&
|
|
(filtered[2].node as Procedure).name.text.endsWith("\$FfiNative")) {
|
|
return filtered[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// TODO(jensj): Possibly move the file to package:front_end and use
|
|
// lowering_predicates.dart.
|
|
static DartScope2? _looksLikeLateLoweredField(List<DartScope2> filtered) {
|
|
if (filtered.length > 10 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[1].node is FunctionNode &&
|
|
filtered[2].node is ReturnStatement &&
|
|
filtered[3].node is Let &&
|
|
filtered[4].node is VariableDeclaration) {
|
|
// The 10 is not special. This has just been observed to contain lots of
|
|
// scopes.
|
|
return filtered[0];
|
|
} else if (filtered.length > 10 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[1].node is FunctionNode &&
|
|
filtered[2].node is ReturnStatement) {
|
|
// The 10 is not special. This has just been observed to contain lots of
|
|
// scopes.
|
|
for (int i = 3; i < filtered.length - 1; i++) {
|
|
if (filtered[i].node is Let &&
|
|
filtered[i + 1].node is VariableDeclaration) {
|
|
return filtered[0];
|
|
}
|
|
}
|
|
} else if (filtered.length > 15 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[1].node is FunctionNode) {
|
|
// The 15 is not special. This has just been observed to contain lots of
|
|
// scopes.
|
|
for (int i = 3; i < filtered.length - 1; i++) {
|
|
if (filtered[i].node is Procedure &&
|
|
filtered[i + 1].node is FunctionNode &&
|
|
filtered[i + 2].node is ReturnStatement) {
|
|
for (int j = i + 1; j < filtered.length - 1; j++) {
|
|
if (filtered[j].node is Let &&
|
|
filtered[j + 1].node is VariableDeclaration) {
|
|
return filtered[0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// late final field.
|
|
if (filtered.length > 10 &&
|
|
filtered[0].node is Procedure &&
|
|
filtered[1].node is FunctionNode &&
|
|
filtered[2].node is ReturnStatement &&
|
|
filtered[3].node is ConditionalExpression &&
|
|
filtered[4].node is InstanceGet &&
|
|
filtered[5].node is ThisExpression &&
|
|
filtered[6].node is InstanceGet &&
|
|
filtered[7].node is ThisExpression &&
|
|
filtered[8].node is Throw) {
|
|
// The 10 is not special. This has just been observed to contain lots of
|
|
// scopes.
|
|
return filtered[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static DartScope2? _looksLikePatternMatching(List<DartScope2> filtered) {
|
|
if (filtered.length > 5 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
(filtered[0].node as VariableDeclaration).isHoisted) {
|
|
// The 5 is not special. This has just been observed to contain lots of
|
|
// scopes.
|
|
// Pattern matching looks something like this:
|
|
// ```
|
|
// hoisted variable1; // offset x
|
|
// hoisted variable2; // offset y
|
|
// // some initialization stuff for variable1. // offset x
|
|
// // some initialization stuff for variable2. // offset y
|
|
// if (stuff with variable1 ending in the setting of it /* offset x */ &&
|
|
// stuff with variable2 ending in the setting of it /* offset y */) {
|
|
// // body
|
|
// }
|
|
// ```
|
|
// Meaning that even if only seeing a hoisted variable as in scope after
|
|
// it has been written for position y in the above example we'll have
|
|
// variable1 in scope for ~half the scopes.
|
|
// We'll assume we've hit this case if we can find a LogicalExpression
|
|
// followed later by a VariableSet.
|
|
// We'll then pick the VariableSet as the scope.
|
|
int foundLogicalExpressionAt = -1;
|
|
for (int i = 1; i < filtered.length; i++) {
|
|
if (filtered[i].node is LogicalExpression) {
|
|
foundLogicalExpressionAt = i;
|
|
}
|
|
}
|
|
if (foundLogicalExpressionAt >= 0) {
|
|
for (int i = foundLogicalExpressionAt + 1; i < filtered.length; i++) {
|
|
if (filtered[i].node is VariableSet) {
|
|
return filtered[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (filtered.length > 5 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is VariableDeclaration &&
|
|
filtered.last.node is LocalFunctionInvocation) {
|
|
// It's beginning to look a lot like a late lowered nullable pattern
|
|
// matching case.
|
|
VariableDeclaration variable1 = filtered[0].node as VariableDeclaration;
|
|
VariableDeclaration variable2 = filtered[1].node as VariableDeclaration;
|
|
if (variable1.isSynthesized &&
|
|
variable1.name?.startsWith("#") == true &&
|
|
variable2.isSynthesized &&
|
|
variable2.isLowered &&
|
|
variable2.name?.startsWith("#") == true) {
|
|
// Assume so. We'll pick the last one where we have previous variables
|
|
//that already matched in scope.
|
|
return filtered.last;
|
|
}
|
|
}
|
|
if (filtered.length <= 4 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
(filtered[0].node as VariableDeclaration).isHoisted &&
|
|
filtered.last.node is VariableSet &&
|
|
(filtered.last.node as VariableSet).variable == filtered[0].node) {
|
|
return filtered.last;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static DartScope2? _looksLikeLateLoweredLocal(List<DartScope2> filtered) {
|
|
VariableDeclaration? variable1;
|
|
VariableDeclaration? variable2;
|
|
if (filtered.length > 5 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[1].node is VariableDeclaration) {
|
|
// A nullable one, e.g. `late Foo? foo` becomes something like
|
|
// ```
|
|
// Foo? #foo;
|
|
// bool #foo#isSet = false;
|
|
// ```
|
|
variable1 = filtered[0].node as VariableDeclaration;
|
|
variable2 = filtered[1].node as VariableDeclaration;
|
|
} else if (filtered.length > 5 &&
|
|
filtered[0].node is VariableDeclaration &&
|
|
filtered[2].node is VariableDeclaration) {
|
|
// A non-nullable one, e.g. `late Foo foo` becomes something like
|
|
// ```
|
|
// Foo? #foo;
|
|
// Foo #foo#get() => bla bla
|
|
// ```
|
|
variable1 = filtered[0].node as VariableDeclaration;
|
|
variable2 = filtered[2].node as VariableDeclaration;
|
|
}
|
|
if (variable1 != null && variable2 != null) {
|
|
// isLateLoweredLocalName/isLateLoweredLocalSetter/etc is in the CFE so we
|
|
// can't call it from here.
|
|
if (variable1.isLowered &&
|
|
variable1.name?.startsWith("#") == true &&
|
|
variable2.isLowered &&
|
|
variable2.name?.startsWith("#") == true) {
|
|
// Assume it's a late lowering thing with an exuberant amount of nodes
|
|
// with the same offset. Just pick the first one.
|
|
return filtered[0];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static DartScope2? _looksLikeThrowPatternMatchingError(
|
|
List<DartScope2> filtered) {
|
|
if (filtered.length == 5 &&
|
|
filtered[0].node is IfStatement &&
|
|
filtered[1].node is ExpressionStatement &&
|
|
filtered[2].node is Throw &&
|
|
filtered[3].node is ConstructorInvocation &&
|
|
filtered[4].node is StringLiteral) {
|
|
// Something like
|
|
// `var (Foo? a, Foo? b) = someCall();`
|
|
// becomes something like
|
|
// ```
|
|
//if (!(checks for Foo? and lets with assigns etc))
|
|
// throw new StateError("Pattern matching error");
|
|
// ```
|
|
// Pick the if.
|
|
return filtered[0];
|
|
} else if (filtered.length == 6 &&
|
|
filtered[0].node is Block &&
|
|
filtered[1].node is IfStatement &&
|
|
filtered[2].node is ExpressionStatement &&
|
|
filtered[3].node is Throw &&
|
|
filtered[4].node is ConstructorInvocation &&
|
|
filtered[5].node is StringLiteral) {
|
|
// As above, but inside a block, e.g. if used in a for-in loop like
|
|
// ```
|
|
// for (var Foo(:Whatnot? bar, :Whatnot? baz ) in entries) { [...] }
|
|
// ```
|
|
// Pick the if.
|
|
return filtered[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static DartScopeBuilder2 _raw(
|
|
Library library, Uri scriptUri, Class? cls, int offset) {
|
|
DartScopeBuilder2 builder = DartScopeBuilder2._(library, scriptUri, offset);
|
|
if (cls != null) {
|
|
builder.visitClass(cls);
|
|
} else {
|
|
builder.checkClasses = false;
|
|
builder.visitLibrary(library);
|
|
}
|
|
|
|
return builder;
|
|
}
|
|
|
|
static DartScopeBuilder2 _rawNoClass(
|
|
Library library, Uri scriptUri, int offset) {
|
|
DartScopeBuilder2 builder = DartScopeBuilder2._(library, scriptUri, offset);
|
|
builder.visitLibrary(library);
|
|
return builder;
|
|
}
|
|
|
|
static List<DartScope2> findScopeFromOffsetAndClassRawForTesting(
|
|
Library library, Uri scriptUri, Class? cls, int offset) =>
|
|
_raw(library, scriptUri, cls, offset).findScopes;
|
|
}
|